Merge pull request 'Vaibhav_Task-#1177' (#70) from Vaibhav_Task-#1177 into main
Reviewed-on: #70
This commit is contained in:
commit
85d776b60b
@ -70,9 +70,9 @@ class DashboardController extends GetxController {
|
|||||||
ever(projectSelectedRange, (_) => fetchProjectProgress());
|
ever(projectSelectedRange, (_) => fetchProjectProgress());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// =========================
|
// =========================
|
||||||
/// Helper Methods
|
// Helper Methods
|
||||||
/// =========================
|
// =========================
|
||||||
int _getDaysFromRange(String range) {
|
int _getDaysFromRange(String range) {
|
||||||
switch (range) {
|
switch (range) {
|
||||||
case '7D':
|
case '7D':
|
||||||
@ -114,21 +114,28 @@ class DashboardController extends GetxController {
|
|||||||
logSafe('Project chart view toggled to: $isChart', level: LogLevel.debug);
|
logSafe('Project chart view toggled to: $isChart', level: LogLevel.debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// =========================
|
// =========================
|
||||||
/// Manual refresh
|
// Manual Refresh Methods
|
||||||
/// =========================
|
// =========================
|
||||||
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 fetchAllDashboardData();
|
await fetchAllDashboardData();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// =========================
|
Future<void> refreshAttendance() async => fetchRoleWiseAttendance();
|
||||||
/// Fetch all dashboard data
|
Future<void> refreshTasks() async {
|
||||||
/// =========================
|
final projectId = projectController.selectedProjectId.value;
|
||||||
|
if (projectId.isNotEmpty) await fetchDashboardTasks(projectId: projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshProjects() async => fetchProjectProgress();
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Fetch All Dashboard Data
|
||||||
|
// =========================
|
||||||
Future<void> fetchAllDashboardData() async {
|
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('No project selected. Skipping dashboard API calls.',
|
logSafe('No project selected. Skipping dashboard API calls.',
|
||||||
level: LogLevel.warning);
|
level: LogLevel.warning);
|
||||||
@ -143,17 +150,15 @@ class DashboardController extends GetxController {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// =========================
|
// =========================
|
||||||
/// API Calls
|
// API Calls
|
||||||
/// =========================
|
// =========================
|
||||||
Future<void> fetchRoleWiseAttendance() async {
|
Future<void> fetchRoleWiseAttendance() async {
|
||||||
final String projectId = projectController.selectedProjectId.value;
|
final String projectId = projectController.selectedProjectId.value;
|
||||||
|
|
||||||
if (projectId.isEmpty) return;
|
if (projectId.isEmpty) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isAttendanceLoading.value = true;
|
isAttendanceLoading.value = true;
|
||||||
|
|
||||||
final List<dynamic>? response =
|
final List<dynamic>? response =
|
||||||
await ApiService.getDashboardAttendanceOverview(
|
await ApiService.getDashboardAttendanceOverview(
|
||||||
projectId, getAttendanceDays());
|
projectId, getAttendanceDays());
|
||||||
@ -179,16 +184,12 @@ class DashboardController extends GetxController {
|
|||||||
|
|
||||||
Future<void> fetchProjectProgress() async {
|
Future<void> fetchProjectProgress() async {
|
||||||
final String projectId = projectController.selectedProjectId.value;
|
final String projectId = projectController.selectedProjectId.value;
|
||||||
|
|
||||||
if (projectId.isEmpty) return;
|
if (projectId.isEmpty) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isProjectLoading.value = true;
|
isProjectLoading.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getProjectProgress(
|
final response = await ApiService.getProjectProgress(
|
||||||
projectId: projectId,
|
projectId: projectId, days: getProjectDays());
|
||||||
days: getProjectDays(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response != null && response.success) {
|
if (response != null && response.success) {
|
||||||
projectChartData.value =
|
projectChartData.value =
|
||||||
@ -208,11 +209,10 @@ class DashboardController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchDashboardTasks({required String projectId}) async {
|
Future<void> fetchDashboardTasks({required String projectId}) async {
|
||||||
if (projectId.isEmpty) return; // Skip if empty
|
if (projectId.isEmpty) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isTasksLoading.value = true;
|
isTasksLoading.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getDashboardTasks(projectId: projectId);
|
final response = await ApiService.getDashboardTasks(projectId: projectId);
|
||||||
|
|
||||||
if (response != null && response.success) {
|
if (response != null && response.success) {
|
||||||
@ -235,11 +235,10 @@ class DashboardController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchDashboardTeams({required String projectId}) async {
|
Future<void> fetchDashboardTeams({required String projectId}) async {
|
||||||
if (projectId.isEmpty) return; // Skip if empty
|
if (projectId.isEmpty) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isTeamsLoading.value = true;
|
isTeamsLoading.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getDashboardTeams(projectId: projectId);
|
final response = await ApiService.getDashboardTeams(projectId: projectId);
|
||||||
|
|
||||||
if (response != null && response.success) {
|
if (response != null && response.success) {
|
||||||
|
@ -10,12 +10,11 @@ class DocumentController extends GetxController {
|
|||||||
var documents = <DocumentItem>[].obs;
|
var documents = <DocumentItem>[].obs;
|
||||||
var filters = Rxn<DocumentFiltersData>();
|
var filters = Rxn<DocumentFiltersData>();
|
||||||
|
|
||||||
// Selected filters
|
// ✅ Selected filters (multi-select support)
|
||||||
var selectedFilter = "".obs;
|
var selectedUploadedBy = <String>[].obs;
|
||||||
var selectedUploadedBy = "".obs;
|
var selectedCategory = <String>[].obs;
|
||||||
var selectedCategory = "".obs;
|
var selectedType = <String>[].obs;
|
||||||
var selectedType = "".obs;
|
var selectedTag = <String>[].obs;
|
||||||
var selectedTag = "".obs;
|
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
var pageNumber = 1.obs;
|
var pageNumber = 1.obs;
|
||||||
@ -31,6 +30,11 @@ class DocumentController extends GetxController {
|
|||||||
// NEW: search
|
// NEW: search
|
||||||
var searchQuery = ''.obs;
|
var searchQuery = ''.obs;
|
||||||
var searchController = TextEditingController();
|
var searchController = TextEditingController();
|
||||||
|
// New filter fields
|
||||||
|
var isUploadedAt = true.obs;
|
||||||
|
var isVerified = RxnBool();
|
||||||
|
var startDate = Rxn<String>();
|
||||||
|
var endDate = Rxn<String>();
|
||||||
|
|
||||||
// ------------------ API Calls -----------------------
|
// ------------------ API Calls -----------------------
|
||||||
|
|
||||||
@ -157,17 +161,21 @@ class DocumentController extends GetxController {
|
|||||||
|
|
||||||
/// Clear selected filters
|
/// Clear selected filters
|
||||||
void clearFilters() {
|
void clearFilters() {
|
||||||
selectedUploadedBy.value = "";
|
selectedUploadedBy.clear();
|
||||||
selectedCategory.value = "";
|
selectedCategory.clear();
|
||||||
selectedType.value = "";
|
selectedType.clear();
|
||||||
selectedTag.value = "";
|
selectedTag.clear();
|
||||||
|
isUploadedAt.value = true;
|
||||||
|
isVerified.value = null;
|
||||||
|
startDate.value = null;
|
||||||
|
endDate.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if any filters are active (for red dot indicator)
|
/// Check if any filters are active (for red dot indicator)
|
||||||
bool hasActiveFilters() {
|
bool hasActiveFilters() {
|
||||||
return selectedUploadedBy.value.isNotEmpty ||
|
return selectedUploadedBy.isNotEmpty ||
|
||||||
selectedCategory.value.isNotEmpty ||
|
selectedCategory.isNotEmpty ||
|
||||||
selectedType.value.isNotEmpty ||
|
selectedType.isNotEmpty ||
|
||||||
selectedTag.value.isNotEmpty;
|
selectedTag.isNotEmpty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import 'package:marco/helpers/widgets/my_snackbar.dart';
|
|||||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
enum Gender {
|
enum Gender {
|
||||||
male,
|
male,
|
||||||
@ -17,6 +18,8 @@ enum Gender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AddEmployeeController extends MyController {
|
class AddEmployeeController extends MyController {
|
||||||
|
Map<String, dynamic>? editingEmployeeData; // For edit mode
|
||||||
|
|
||||||
List<PlatformFile> files = [];
|
List<PlatformFile> files = [];
|
||||||
final MyFormValidator basicValidator = MyFormValidator();
|
final MyFormValidator basicValidator = MyFormValidator();
|
||||||
Gender? selectedGender;
|
Gender? selectedGender;
|
||||||
@ -33,12 +36,10 @@ class AddEmployeeController extends MyController {
|
|||||||
logSafe("Initializing AddEmployeeController...");
|
logSafe("Initializing AddEmployeeController...");
|
||||||
_initializeFields();
|
_initializeFields();
|
||||||
fetchRoles();
|
fetchRoles();
|
||||||
}
|
|
||||||
|
|
||||||
void setJoiningDate(DateTime date) {
|
if (editingEmployeeData != null) {
|
||||||
joiningDate = date;
|
prefillFields();
|
||||||
logSafe("Joining date selected: $date");
|
}
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initializeFields() {
|
void _initializeFields() {
|
||||||
@ -63,6 +64,37 @@ class AddEmployeeController extends MyController {
|
|||||||
logSafe("Fields initialized for first_name, phone_number, last_name.");
|
logSafe("Fields initialized for first_name, phone_number, last_name.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prefill fields in edit mode
|
||||||
|
// In AddEmployeeController
|
||||||
|
void prefillFields() {
|
||||||
|
logSafe("Prefilling data for editing...");
|
||||||
|
basicValidator.getController('first_name')?.text =
|
||||||
|
editingEmployeeData?['first_name'] ?? '';
|
||||||
|
basicValidator.getController('last_name')?.text =
|
||||||
|
editingEmployeeData?['last_name'] ?? '';
|
||||||
|
basicValidator.getController('phone_number')?.text =
|
||||||
|
editingEmployeeData?['phone_number'] ?? '';
|
||||||
|
|
||||||
|
selectedGender = editingEmployeeData?['gender'] != null
|
||||||
|
? Gender.values
|
||||||
|
.firstWhereOrNull((g) => g.name == editingEmployeeData!['gender'])
|
||||||
|
: null;
|
||||||
|
|
||||||
|
selectedRoleId = editingEmployeeData?['job_role_id'];
|
||||||
|
|
||||||
|
if (editingEmployeeData?['joining_date'] != null) {
|
||||||
|
joiningDate = DateTime.tryParse(editingEmployeeData!['joining_date']);
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setJoiningDate(DateTime date) {
|
||||||
|
joiningDate = date;
|
||||||
|
logSafe("Joining date selected: $date");
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
void onGenderSelected(Gender? gender) {
|
void onGenderSelected(Gender? gender) {
|
||||||
selectedGender = gender;
|
selectedGender = gender;
|
||||||
logSafe("Gender selected: ${gender?.name}");
|
logSafe("Gender selected: ${gender?.name}");
|
||||||
@ -92,10 +124,13 @@ class AddEmployeeController extends MyController {
|
|||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>?> createEmployees() async {
|
/// Create or update employee
|
||||||
logSafe("Starting employee creation...");
|
Future<Map<String, dynamic>?> createOrUpdateEmployee() async {
|
||||||
|
logSafe(editingEmployeeData != null
|
||||||
|
? "Starting employee update..."
|
||||||
|
: "Starting employee creation...");
|
||||||
|
|
||||||
if (selectedGender == null || selectedRoleId == null) {
|
if (selectedGender == null || selectedRoleId == null) {
|
||||||
logSafe("Missing gender or role.", level: LogLevel.warning);
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Missing Fields",
|
title: "Missing Fields",
|
||||||
message: "Please select both Gender and Role.",
|
message: "Please select both Gender and Role.",
|
||||||
@ -111,6 +146,7 @@ class AddEmployeeController extends MyController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await ApiService.createEmployee(
|
final response = await ApiService.createEmployee(
|
||||||
|
id: editingEmployeeData?['id'], // Pass id if editing
|
||||||
firstName: firstName!,
|
firstName: firstName!,
|
||||||
lastName: lastName!,
|
lastName: lastName!,
|
||||||
phoneNumber: phoneNumber!,
|
phoneNumber: phoneNumber!,
|
||||||
@ -122,25 +158,25 @@ class AddEmployeeController extends MyController {
|
|||||||
logSafe("Response: $response");
|
logSafe("Response: $response");
|
||||||
|
|
||||||
if (response != null && response['success'] == true) {
|
if (response != null && response['success'] == true) {
|
||||||
logSafe("Employee created successfully.");
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Success",
|
title: "Success",
|
||||||
message: "Employee created successfully!",
|
message: editingEmployeeData != null
|
||||||
|
? "Employee updated successfully!"
|
||||||
|
: "Employee created successfully!",
|
||||||
type: SnackbarType.success,
|
type: SnackbarType.success,
|
||||||
);
|
);
|
||||||
return response;
|
return response;
|
||||||
} else {
|
} else {
|
||||||
logSafe("Failed to create employee (response false)",
|
logSafe("Failed operation", level: LogLevel.error);
|
||||||
level: LogLevel.error);
|
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
logSafe("Error creating employee",
|
logSafe("Error creating/updating employee",
|
||||||
level: LogLevel.error, error: e, stackTrace: st);
|
level: LogLevel.error, error: e, stackTrace: st);
|
||||||
}
|
}
|
||||||
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: "Failed to create employee.",
|
message: "Failed to save employee.",
|
||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
|
@ -1880,6 +1880,7 @@ class ApiService {
|
|||||||
_getRequest(ApiEndpoints.getRoles).then(
|
_getRequest(ApiEndpoints.getRoles).then(
|
||||||
(res) => res != null ? _parseResponse(res, label: 'Roles') : null);
|
(res) => res != null ? _parseResponse(res, label: 'Roles') : null);
|
||||||
static Future<Map<String, dynamic>?> createEmployee({
|
static Future<Map<String, dynamic>?> createEmployee({
|
||||||
|
String? id, // Optional, for editing
|
||||||
required String firstName,
|
required String firstName,
|
||||||
required String lastName,
|
required String lastName,
|
||||||
required String phoneNumber,
|
required String phoneNumber,
|
||||||
@ -1888,12 +1889,13 @@ class ApiService {
|
|||||||
required String joiningDate,
|
required String joiningDate,
|
||||||
}) async {
|
}) async {
|
||||||
final body = {
|
final body = {
|
||||||
|
if (id != null) "id": id, // Include id only if editing
|
||||||
"firstName": firstName,
|
"firstName": firstName,
|
||||||
"lastName": lastName,
|
"lastName": lastName,
|
||||||
"phoneNumber": phoneNumber,
|
"phoneNumber": phoneNumber,
|
||||||
"gender": gender,
|
"gender": gender,
|
||||||
"jobRoleId": jobRoleId,
|
"jobRoleId": jobRoleId,
|
||||||
"joiningDate": joiningDate
|
"joiningDate": joiningDate,
|
||||||
};
|
};
|
||||||
|
|
||||||
final response = await _postRequest(
|
final response = await _postRequest(
|
||||||
@ -1907,7 +1909,7 @@ class ApiService {
|
|||||||
final json = jsonDecode(response.body);
|
final json = jsonDecode(response.body);
|
||||||
return {
|
return {
|
||||||
"success": response.statusCode == 200 && json['success'] == true,
|
"success": response.statusCode == 200 && json['success'] == true,
|
||||||
"data": json
|
"data": json,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,8 +9,9 @@ import 'package:marco/controller/expense/expense_detail_controller.dart';
|
|||||||
import 'package:marco/controller/directory/directory_controller.dart';
|
import 'package:marco/controller/directory/directory_controller.dart';
|
||||||
import 'package:marco/controller/directory/notes_controller.dart';
|
import 'package:marco/controller/directory/notes_controller.dart';
|
||||||
import 'package:marco/controller/document/user_document_controller.dart';
|
import 'package:marco/controller/document/user_document_controller.dart';
|
||||||
import 'package:marco/helpers/utils/permission_constants.dart';
|
|
||||||
import 'package:marco/controller/document/document_details_controller.dart';
|
import 'package:marco/controller/document/document_details_controller.dart';
|
||||||
|
import 'package:marco/controller/dashboard/dashboard_controller.dart';
|
||||||
|
import 'package:marco/helpers/utils/permission_constants.dart';
|
||||||
|
|
||||||
/// Handles incoming FCM notification actions and updates UI/controllers.
|
/// Handles incoming FCM notification actions and updates UI/controllers.
|
||||||
class NotificationActionHandler {
|
class NotificationActionHandler {
|
||||||
@ -46,6 +47,10 @@ class NotificationActionHandler {
|
|||||||
break;
|
break;
|
||||||
case 'attendance_updated':
|
case 'attendance_updated':
|
||||||
_handleAttendanceUpdated(data);
|
_handleAttendanceUpdated(data);
|
||||||
|
_handleDashboardUpdate(data); // refresh dashboard attendance
|
||||||
|
break;
|
||||||
|
case 'dashboard_update':
|
||||||
|
_handleDashboardUpdate(data); // full dashboard refresh
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
_logger.w('⚠️ Unknown notification type: $type');
|
_logger.w('⚠️ Unknown notification type: $type');
|
||||||
@ -60,16 +65,19 @@ class NotificationActionHandler {
|
|||||||
case 'Attendance':
|
case 'Attendance':
|
||||||
if (_isAttendanceAction(action)) {
|
if (_isAttendanceAction(action)) {
|
||||||
_handleAttendanceUpdated(data);
|
_handleAttendanceUpdated(data);
|
||||||
|
_handleDashboardUpdate(data);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
/// 🔹 Tasks
|
/// 🔹 Tasks
|
||||||
case 'Report_Task':
|
case 'Report_Task':
|
||||||
_handleTaskUpdated(data, isComment: false);
|
_handleTaskUpdated(data, isComment: false);
|
||||||
|
_handleDashboardUpdate(data);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Task_Comment':
|
case 'Task_Comment':
|
||||||
_handleTaskUpdated(data, isComment: true);
|
_handleTaskUpdated(data, isComment: true);
|
||||||
|
_handleDashboardUpdate(data);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Task_Modified':
|
case 'Task_Modified':
|
||||||
@ -77,11 +85,13 @@ class NotificationActionHandler {
|
|||||||
case 'Floor_Modified':
|
case 'Floor_Modified':
|
||||||
case 'Building_Modified':
|
case 'Building_Modified':
|
||||||
_handleTaskPlanningUpdated(data);
|
_handleTaskPlanningUpdated(data);
|
||||||
|
_handleDashboardUpdate(data);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
/// 🔹 Expenses
|
/// 🔹 Expenses
|
||||||
case 'Expenses_Modified':
|
case 'Expenses_Modified':
|
||||||
_handleExpenseUpdated(data);
|
_handleExpenseUpdated(data);
|
||||||
|
_handleDashboardUpdate(data);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
/// 🔹 Documents
|
/// 🔹 Documents
|
||||||
@ -198,88 +208,103 @@ class NotificationActionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// ---------------------- DOCUMENT HANDLER ----------------------
|
/// ---------------------- DOCUMENT HANDLER ----------------------
|
||||||
/// ---------------------- DOCUMENT HANDLER ----------------------
|
static void _handleDocumentModified(Map<String, dynamic> data) {
|
||||||
static void _handleDocumentModified(Map<String, dynamic> data) {
|
String entityTypeId;
|
||||||
late String entityTypeId;
|
String entityId;
|
||||||
late String entityId;
|
String? documentId = data['DocumentId'];
|
||||||
String? documentId = data['DocumentId'];
|
|
||||||
|
|
||||||
if (data['Keyword'] == 'Employee_Document_Modified') {
|
// Determine entity type and ID
|
||||||
entityTypeId = Permissions.employeeEntity;
|
if (data['Keyword'] == 'Employee_Document_Modified') {
|
||||||
entityId = data['EmployeeId'] ?? '';
|
entityTypeId = Permissions.employeeEntity;
|
||||||
} else if (data['Keyword'] == 'Project_Document_Modified') {
|
entityId = data['EmployeeId'] ?? '';
|
||||||
entityTypeId = Permissions.projectEntity;
|
} else if (data['Keyword'] == 'Project_Document_Modified') {
|
||||||
entityId = data['ProjectId'] ?? '';
|
entityTypeId = Permissions.projectEntity;
|
||||||
} else {
|
entityId = data['ProjectId'] ?? '';
|
||||||
_logger.w("⚠️ Document update received with unknown keyword: $data");
|
} else {
|
||||||
return;
|
_logger.w("⚠️ Document update received with unknown keyword: $data");
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (entityId.isEmpty) {
|
if (entityId.isEmpty) {
|
||||||
_logger.w("⚠️ Document update missing entityId: $data");
|
_logger.w("⚠️ Document update missing entityId: $data");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔹 Refresh document list
|
_logger.i(
|
||||||
_safeControllerUpdate<DocumentController>(
|
"🔔 Document notification received: keyword=${data['Keyword']}, entityTypeId=$entityTypeId, entityId=$entityId, documentId=$documentId");
|
||||||
onFound: (controller) async {
|
|
||||||
await controller.fetchDocuments(
|
// Refresh Document List
|
||||||
entityTypeId: entityTypeId,
|
if (Get.isRegistered<DocumentController>()) {
|
||||||
entityId: entityId,
|
_safeControllerUpdate<DocumentController>(
|
||||||
reset: true,
|
onFound: (controller) async {
|
||||||
|
await controller.fetchDocuments(
|
||||||
|
entityTypeId: entityTypeId,
|
||||||
|
entityId: entityId,
|
||||||
|
reset: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
notFoundMessage:
|
||||||
|
'⚠️ DocumentController not found, cannot refresh list.',
|
||||||
|
successMessage: '✅ DocumentController refreshed from notification.',
|
||||||
);
|
);
|
||||||
},
|
} else {
|
||||||
notFoundMessage: '⚠️ DocumentController not found, cannot refresh list.',
|
_logger.w('⚠️ DocumentController not registered, skipping list refresh.');
|
||||||
successMessage: '✅ DocumentController refreshed from notification.',
|
}
|
||||||
);
|
|
||||||
|
|
||||||
// 🔹 Refresh document details (if opened)
|
// Refresh Document Details (if open)
|
||||||
if (documentId != null) {
|
if (documentId != null && Get.isRegistered<DocumentDetailsController>()) {
|
||||||
_safeControllerUpdate<DocumentDetailsController>(
|
_safeControllerUpdate<DocumentDetailsController>(
|
||||||
onFound: (controller) async {
|
onFound: (controller) async {
|
||||||
if (controller.documentDetails.value?.data?.id == documentId) {
|
// Refresh details regardless of current document
|
||||||
await controller.fetchDocumentDetails(documentId);
|
await controller.fetchDocumentDetails(documentId);
|
||||||
_logger.i("✅ DocumentDetailsController refreshed for Document $documentId");
|
_logger.i(
|
||||||
}
|
"✅ DocumentDetailsController refreshed for Document $documentId");
|
||||||
},
|
},
|
||||||
notFoundMessage: 'ℹ️ DocumentDetailsController not active, skipping.',
|
notFoundMessage:
|
||||||
successMessage: '✅ DocumentDetailsController checked for refresh.',
|
'ℹ️ DocumentDetailsController not active, skipping details refresh.',
|
||||||
);
|
successMessage: '✅ DocumentDetailsController checked for refresh.',
|
||||||
|
);
|
||||||
|
} else if (documentId != null) {
|
||||||
|
_logger.w(
|
||||||
|
'⚠️ DocumentDetailsController not registered, cannot refresh document details.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// ---------------------- DIRECTORY HANDLERS ----------------------
|
/// ---------------------- DIRECTORY HANDLERS ----------------------
|
||||||
|
/// ---------------------- DIRECTORY HANDLERS ----------------------
|
||||||
static void _handleContactModified(Map<String, dynamic> data) {
|
static void _handleContactModified(Map<String, dynamic> data) {
|
||||||
_safeControllerUpdate<DirectoryController>(
|
final contactId = data['ContactId'];
|
||||||
onFound: (controller) => controller.fetchContacts(),
|
|
||||||
notFoundMessage: '⚠️ DirectoryController not found, cannot refresh.',
|
|
||||||
successMessage: '✅ Directory contacts refreshed from notification.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void _handleContactNoteModified(Map<String, dynamic> data) {
|
|
||||||
final contactId = data['contactId'];
|
|
||||||
|
|
||||||
|
// Always refresh the contact list
|
||||||
_safeControllerUpdate<DirectoryController>(
|
_safeControllerUpdate<DirectoryController>(
|
||||||
onFound: (controller) {
|
onFound: (controller) {
|
||||||
|
controller.fetchContacts();
|
||||||
|
// If a specific contact is provided, refresh its notes as well
|
||||||
if (contactId != null) {
|
if (contactId != null) {
|
||||||
controller.fetchCommentsForContact(contactId);
|
controller.fetchCommentsForContact(contactId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
notFoundMessage:
|
notFoundMessage:
|
||||||
'⚠️ DirectoryController not found, cannot refresh notes.',
|
'⚠️ DirectoryController not found, cannot refresh contacts.',
|
||||||
successMessage: '✅ Directory comments refreshed from notification.',
|
successMessage:
|
||||||
|
'✅ Directory contacts (and notes if applicable) refreshed from notification.',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Refresh notes globally as well
|
||||||
_safeControllerUpdate<NotesController>(
|
_safeControllerUpdate<NotesController>(
|
||||||
onFound: (controller) => controller.fetchNotes(),
|
onFound: (controller) => controller.fetchNotes(),
|
||||||
notFoundMessage: '⚠️ NotesController not found, cannot refresh.',
|
notFoundMessage: '⚠️ NotesController not found, cannot refresh notes.',
|
||||||
successMessage: '✅ Notes refreshed from notification.',
|
successMessage: '✅ Notes refreshed from notification.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void _handleContactNoteModified(Map<String, dynamic> data) {
|
||||||
|
final contactId = data['ContactId'];
|
||||||
|
|
||||||
|
// Refresh both contacts and notes when a note is modified
|
||||||
|
_handleContactModified(data);
|
||||||
|
}
|
||||||
|
|
||||||
static void _handleBucketModified(Map<String, dynamic> data) {
|
static void _handleBucketModified(Map<String, dynamic> data) {
|
||||||
_safeControllerUpdate<DirectoryController>(
|
_safeControllerUpdate<DirectoryController>(
|
||||||
onFound: (controller) => controller.fetchBuckets(),
|
onFound: (controller) => controller.fetchBuckets(),
|
||||||
@ -296,6 +321,33 @@ static void _handleDocumentModified(Map<String, dynamic> data) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ---------------------- DASHBOARD HANDLER ----------------------
|
||||||
|
static void _handleDashboardUpdate(Map<String, dynamic> data) {
|
||||||
|
_safeControllerUpdate<DashboardController>(
|
||||||
|
onFound: (controller) async {
|
||||||
|
final type = data['type'] ?? '';
|
||||||
|
switch (type) {
|
||||||
|
case 'attendance_updated':
|
||||||
|
await controller.fetchRoleWiseAttendance();
|
||||||
|
break;
|
||||||
|
case 'task_updated':
|
||||||
|
await controller.fetchDashboardTasks(
|
||||||
|
projectId:
|
||||||
|
controller.projectController.selectedProjectId.value);
|
||||||
|
break;
|
||||||
|
case 'project_progress_update':
|
||||||
|
await controller.fetchProjectProgress();
|
||||||
|
break;
|
||||||
|
case 'full_dashboard_refresh':
|
||||||
|
default:
|
||||||
|
await controller.refreshDashboard();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
notFoundMessage: '⚠️ DashboardController not found, cannot refresh.',
|
||||||
|
successMessage: '✅ DashboardController refreshed from notification.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// ---------------------- UTILITY ----------------------
|
/// ---------------------- UTILITY ----------------------
|
||||||
|
|
||||||
static void _safeControllerUpdate<T>({
|
static void _safeControllerUpdate<T>({
|
||||||
|
@ -154,8 +154,10 @@ class TileContainer extends StatelessWidget {
|
|||||||
const TileContainer({required this.child, Key? key}) : super(key: key);
|
const TileContainer({required this.child, Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) =>
|
Widget build(BuildContext context) => Container(
|
||||||
Container(padding: const EdgeInsets.all(14), decoration: _tileDecoration, child: child);
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: _tileDecoration,
|
||||||
|
child: child);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ==========================
|
/// ==========================
|
||||||
@ -187,9 +189,8 @@ class AttachmentsSection extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
final activeExisting = existingAttachments
|
final activeExisting =
|
||||||
.where((doc) => doc['isActive'] != false)
|
existingAttachments.where((doc) => doc['isActive'] != false).toList();
|
||||||
.toList();
|
|
||||||
|
|
||||||
final imageFiles = attachments.where(_isImageFile).toList();
|
final imageFiles = attachments.where(_isImageFile).toList();
|
||||||
final imageExisting = activeExisting
|
final imageExisting = activeExisting
|
||||||
@ -336,8 +337,8 @@ class AttachmentsSection extends StatelessWidget {
|
|||||||
Widget _buildActionTile(IconData icon, VoidCallback onTap) => GestureDetector(
|
Widget _buildActionTile(IconData icon, VoidCallback onTap) => GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 80,
|
width: 50,
|
||||||
height: 80,
|
height: 50,
|
||||||
decoration: _tileDecoration.copyWith(
|
decoration: _tileDecoration.copyWith(
|
||||||
border: Border.all(color: Colors.grey.shade400),
|
border: Border.all(color: Colors.grey.shade400),
|
||||||
),
|
),
|
||||||
@ -359,7 +360,8 @@ class _AttachmentTile extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final fileName = file.path.split('/').last;
|
final fileName = file.path.split('/').last;
|
||||||
final extension = fileName.split('.').last.toLowerCase();
|
final extension = fileName.split('.').last.toLowerCase();
|
||||||
final isImage = AttachmentsSection.allowedImageExtensions.contains(extension);
|
final isImage =
|
||||||
|
AttachmentsSection.allowedImageExtensions.contains(extension);
|
||||||
|
|
||||||
final (icon, color) = _fileIcon(extension);
|
final (icon, color) = _fileIcon(extension);
|
||||||
|
|
||||||
|
@ -129,6 +129,7 @@ class WorkItem {
|
|||||||
final WorkCategoryMaster? workCategoryMaster;
|
final WorkCategoryMaster? workCategoryMaster;
|
||||||
final double? plannedWork;
|
final double? plannedWork;
|
||||||
final double? completedWork;
|
final double? completedWork;
|
||||||
|
final double? todaysAssigned;
|
||||||
final DateTime? taskDate;
|
final DateTime? taskDate;
|
||||||
final String? tenantId;
|
final String? tenantId;
|
||||||
final Tenant? tenant;
|
final Tenant? tenant;
|
||||||
@ -143,6 +144,7 @@ class WorkItem {
|
|||||||
this.workCategoryMaster,
|
this.workCategoryMaster,
|
||||||
this.plannedWork,
|
this.plannedWork,
|
||||||
this.completedWork,
|
this.completedWork,
|
||||||
|
this.todaysAssigned,
|
||||||
this.taskDate,
|
this.taskDate,
|
||||||
this.tenantId,
|
this.tenantId,
|
||||||
this.tenant,
|
this.tenant,
|
||||||
@ -171,6 +173,9 @@ class WorkItem {
|
|||||||
completedWork: json['completedWork'] != null
|
completedWork: json['completedWork'] != null
|
||||||
? (json['completedWork'] as num).toDouble()
|
? (json['completedWork'] as num).toDouble()
|
||||||
: null,
|
: null,
|
||||||
|
todaysAssigned: json['todaysAssigned'] != null
|
||||||
|
? (json['todaysAssigned'] as num).toDouble()
|
||||||
|
: null, // ✅ added parsing
|
||||||
taskDate:
|
taskDate:
|
||||||
json['taskDate'] != null ? DateTime.tryParse(json['taskDate']) : null,
|
json['taskDate'] != null ? DateTime.tryParse(json['taskDate']) : null,
|
||||||
tenantId: json['tenantId'] as String?,
|
tenantId: json['tenantId'] as String?,
|
||||||
|
@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/controller/document/user_document_controller.dart';
|
import 'package:marco/controller/document/user_document_controller.dart';
|
||||||
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
||||||
|
import 'package:marco/helpers/utils/date_time_utils.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/widgets/my_text_style.dart';
|
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||||
import 'package:marco/model/document/document_filter_model.dart';
|
import 'package:marco/model/document/document_filter_model.dart';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
class UserDocumentFilterBottomSheet extends StatelessWidget {
|
class UserDocumentFilterBottomSheet extends StatelessWidget {
|
||||||
final String entityId;
|
final String entityId;
|
||||||
@ -36,15 +38,21 @@ class UserDocumentFilterBottomSheet extends StatelessWidget {
|
|||||||
onCancel: () => Get.back(),
|
onCancel: () => Get.back(),
|
||||||
onSubmit: () {
|
onSubmit: () {
|
||||||
final combinedFilter = {
|
final combinedFilter = {
|
||||||
'uploadedBy': docController.selectedUploadedBy.value,
|
'uploadedByIds': docController.selectedUploadedBy.toList(),
|
||||||
'category': docController.selectedCategory.value,
|
'documentCategoryIds': docController.selectedCategory.toList(),
|
||||||
'type': docController.selectedType.value,
|
'documentTypeIds': docController.selectedType.toList(),
|
||||||
'tag': docController.selectedTag.value,
|
'documentTagIds': docController.selectedTag.toList(),
|
||||||
|
'isUploadedAt': docController.isUploadedAt.value,
|
||||||
|
'startDate': docController.startDate.value,
|
||||||
|
'endDate': docController.endDate.value,
|
||||||
|
if (docController.isVerified.value != null)
|
||||||
|
'isVerified': docController.isVerified.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
docController.fetchDocuments(
|
docController.fetchDocuments(
|
||||||
entityTypeId: entityTypeId,
|
entityTypeId: entityTypeId,
|
||||||
entityId: entityId,
|
entityId: entityId,
|
||||||
filter: combinedFilter.toString(),
|
filter: jsonEncode(combinedFilter),
|
||||||
reset: true,
|
reset: true,
|
||||||
);
|
);
|
||||||
Get.back();
|
Get.back();
|
||||||
@ -67,32 +75,209 @@ class UserDocumentFilterBottomSheet extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// --- Date Filter (Uploaded On / Updated On) ---
|
||||||
|
_buildField(
|
||||||
|
"Choose Date",
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Segmented Buttons
|
||||||
|
Obx(() {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () =>
|
||||||
|
docController.isUploadedAt.value = true,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: docController.isUploadedAt.value
|
||||||
|
? Colors.indigo.shade400
|
||||||
|
: Colors.transparent,
|
||||||
|
borderRadius:
|
||||||
|
const BorderRadius.horizontal(
|
||||||
|
left: Radius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: MyText(
|
||||||
|
"Uploaded On",
|
||||||
|
style: MyTextStyle.bodyMedium(
|
||||||
|
color:
|
||||||
|
docController.isUploadedAt.value
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black87,
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => docController
|
||||||
|
.isUploadedAt.value = false,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: !docController.isUploadedAt.value
|
||||||
|
? Colors.indigo.shade400
|
||||||
|
: Colors.transparent,
|
||||||
|
borderRadius:
|
||||||
|
const BorderRadius.horizontal(
|
||||||
|
right: Radius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: MyText(
|
||||||
|
"Updated On",
|
||||||
|
style: MyTextStyle.bodyMedium(
|
||||||
|
color: !docController
|
||||||
|
.isUploadedAt.value
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black87,
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
MySpacing.height(12),
|
||||||
|
|
||||||
|
// Date Range
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Obx(() {
|
||||||
|
return _dateButton(
|
||||||
|
label: docController.startDate.value == null
|
||||||
|
? 'Start Date'
|
||||||
|
: DateTimeUtils.formatDate(
|
||||||
|
DateTime.parse(
|
||||||
|
docController.startDate.value!),
|
||||||
|
'dd MMM yyyy',
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: DateTime.now(),
|
||||||
|
firstDate: DateTime(2000),
|
||||||
|
lastDate: DateTime.now(),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
docController.startDate.value =
|
||||||
|
picked.toIso8601String();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
MySpacing.width(12),
|
||||||
|
Expanded(
|
||||||
|
child: Obx(() {
|
||||||
|
return _dateButton(
|
||||||
|
label: docController.endDate.value == null
|
||||||
|
? 'End Date'
|
||||||
|
: DateTimeUtils.formatDate(
|
||||||
|
DateTime.parse(
|
||||||
|
docController.endDate.value!),
|
||||||
|
'dd MMM yyyy',
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: DateTime.now(),
|
||||||
|
firstDate: DateTime(2000),
|
||||||
|
lastDate: DateTime.now(),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
docController.endDate.value =
|
||||||
|
picked.toIso8601String();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
_buildDynamicField(
|
_multiSelectField(
|
||||||
label: "Uploaded By",
|
label: "Uploaded By",
|
||||||
items: filterData.uploadedBy,
|
items: filterData.uploadedBy,
|
||||||
fallback: "Select Uploaded By",
|
fallback: "Select Uploaded By",
|
||||||
selectedValue: docController.selectedUploadedBy,
|
selectedValues: docController.selectedUploadedBy,
|
||||||
),
|
),
|
||||||
_buildDynamicField(
|
_multiSelectField(
|
||||||
label: "Category",
|
label: "Category",
|
||||||
items: filterData.documentCategory,
|
items: filterData.documentCategory,
|
||||||
fallback: "Select Category",
|
fallback: "Select Category",
|
||||||
selectedValue: docController.selectedCategory,
|
selectedValues: docController.selectedCategory,
|
||||||
),
|
),
|
||||||
_buildDynamicField(
|
_multiSelectField(
|
||||||
label: "Type",
|
label: "Type",
|
||||||
items: filterData.documentType,
|
items: filterData.documentType,
|
||||||
fallback: "Select Type",
|
fallback: "Select Type",
|
||||||
selectedValue: docController.selectedType,
|
selectedValues: docController.selectedType,
|
||||||
),
|
),
|
||||||
_buildDynamicField(
|
_multiSelectField(
|
||||||
label: "Tag",
|
label: "Tag",
|
||||||
items: filterData.documentTag,
|
items: filterData.documentTag,
|
||||||
fallback: "Select Tag",
|
fallback: "Select Tag",
|
||||||
selectedValue: docController.selectedTag,
|
selectedValues: docController.selectedTag,
|
||||||
),
|
),
|
||||||
].where((w) => w != null).cast<Widget>().toList(),
|
|
||||||
|
// --- Verified Toggle ---
|
||||||
|
_buildField(
|
||||||
|
"Only Verified",
|
||||||
|
Obx(() {
|
||||||
|
return Container(
|
||||||
|
padding: MySpacing.xy(12, 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
MyText(
|
||||||
|
"Verified Documents Only",
|
||||||
|
style: MyTextStyle.bodyMedium(),
|
||||||
|
),
|
||||||
|
Switch(
|
||||||
|
value: docController.isVerified.value ?? false,
|
||||||
|
onChanged: (val) {
|
||||||
|
docController.isVerified.value =
|
||||||
|
val ? true : null;
|
||||||
|
},
|
||||||
|
activeColor: Colors.indigo,
|
||||||
|
materialTapTargetSize:
|
||||||
|
MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
: Center(
|
: Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -110,70 +295,161 @@ class UserDocumentFilterBottomSheet extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget? _buildDynamicField({
|
Widget _multiSelectField({
|
||||||
required String label,
|
required String label,
|
||||||
required List<FilterItem> items,
|
required List<FilterItem> items,
|
||||||
required String fallback,
|
required String fallback,
|
||||||
required RxString selectedValue,
|
required RxList<String> selectedValues,
|
||||||
}) {
|
}) {
|
||||||
if (items.isEmpty) return null;
|
if (items.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MyText.labelMedium(label),
|
MyText.labelMedium(label),
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
_popupSelector(items, fallback, selectedValue: selectedValue),
|
Obx(() {
|
||||||
|
final selectedNames = items
|
||||||
|
.where((f) => selectedValues.contains(f.id))
|
||||||
|
.map((f) => f.name)
|
||||||
|
.join(", ");
|
||||||
|
final displayText =
|
||||||
|
selectedNames.isNotEmpty ? selectedNames : fallback;
|
||||||
|
|
||||||
|
return Builder(
|
||||||
|
builder: (context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
final RenderBox button =
|
||||||
|
context.findRenderObject() as RenderBox;
|
||||||
|
final RenderBox overlay = Overlay.of(context)
|
||||||
|
.context
|
||||||
|
.findRenderObject() as RenderBox;
|
||||||
|
|
||||||
|
final position = button.localToGlobal(Offset.zero);
|
||||||
|
|
||||||
|
await showMenu(
|
||||||
|
context: context,
|
||||||
|
position: RelativeRect.fromLTRB(
|
||||||
|
position.dx,
|
||||||
|
position.dy + button.size.height,
|
||||||
|
overlay.size.width - position.dx - button.size.width,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
items: items.map(
|
||||||
|
(f) {
|
||||||
|
return PopupMenuItem<String>(
|
||||||
|
enabled: false, // prevent auto-close
|
||||||
|
child: StatefulBuilder(
|
||||||
|
builder: (context, setState) {
|
||||||
|
final isChecked = selectedValues.contains(f.id);
|
||||||
|
return CheckboxListTile(
|
||||||
|
dense: true,
|
||||||
|
value: isChecked,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
controlAffinity:
|
||||||
|
ListTileControlAffinity.leading,
|
||||||
|
title: MyText(f.name),
|
||||||
|
|
||||||
|
// --- Styles ---
|
||||||
|
checkColor: Colors.white, // tick color
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Colors.black,
|
||||||
|
width: 1.5), // border when unchecked
|
||||||
|
|
||||||
|
fillColor:
|
||||||
|
MaterialStateProperty.resolveWith<Color>(
|
||||||
|
(states) {
|
||||||
|
if (states
|
||||||
|
.contains(MaterialState.selected)) {
|
||||||
|
return Colors.indigo; // checked → Indigo
|
||||||
|
}
|
||||||
|
return Colors.white; // unchecked → White
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
onChanged: (val) {
|
||||||
|
if (val == true) {
|
||||||
|
selectedValues.add(f.id);
|
||||||
|
} else {
|
||||||
|
selectedValues.remove(f.id);
|
||||||
|
}
|
||||||
|
setState(() {}); // refresh UI
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: MySpacing.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: MyText(
|
||||||
|
displayText,
|
||||||
|
style: const TextStyle(color: Colors.black87),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Icon(Icons.arrow_drop_down, color: Colors.grey),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _popupSelector(
|
Widget _buildField(String label, Widget child) {
|
||||||
List<FilterItem> items,
|
return Column(
|
||||||
String fallback, {
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
required RxString selectedValue,
|
children: [
|
||||||
}) {
|
MyText.labelMedium(label),
|
||||||
return Obx(() {
|
MySpacing.height(8),
|
||||||
final currentValue = _getCurrentName(selectedValue.value, items, fallback);
|
child,
|
||||||
return PopupMenuButton<String>(
|
MySpacing.height(8),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
],
|
||||||
onSelected: (val) => selectedValue.value = val,
|
);
|
||||||
itemBuilder: (context) => items
|
|
||||||
.map(
|
|
||||||
(f) => PopupMenuItem<String>(
|
|
||||||
value: f.id,
|
|
||||||
child: MyText(f.name),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
child: Container(
|
|
||||||
padding: MySpacing.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade100,
|
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: MyText(
|
|
||||||
currentValue,
|
|
||||||
style: const TextStyle(color: Colors.black87),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Icon(Icons.arrow_drop_down, color: Colors.grey),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getCurrentName(String selectedId, List<FilterItem> list, String fallback) {
|
Widget _dateButton({required String label, required VoidCallback onTap}) {
|
||||||
if (selectedId.isEmpty) return fallback;
|
return GestureDetector(
|
||||||
final match = list.firstWhereOrNull((f) => f.id == selectedId);
|
onTap: onTap,
|
||||||
return match?.name ?? fallback;
|
child: Container(
|
||||||
|
padding: MySpacing.xy(16, 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.calendar_today, size: 16, color: Colors.grey),
|
||||||
|
MySpacing.width(8),
|
||||||
|
Expanded(
|
||||||
|
child: MyText(
|
||||||
|
label,
|
||||||
|
style: MyTextStyle.bodyMedium(),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,15 +11,31 @@ import 'package:marco/helpers/utils/base_bottom_sheet.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';
|
||||||
|
|
||||||
|
|
||||||
class AddEmployeeBottomSheet extends StatefulWidget {
|
class AddEmployeeBottomSheet extends StatefulWidget {
|
||||||
|
final Map<String, dynamic>? employeeData;
|
||||||
|
AddEmployeeBottomSheet({this.employeeData});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AddEmployeeBottomSheet> createState() => _AddEmployeeBottomSheetState();
|
State<AddEmployeeBottomSheet> createState() => _AddEmployeeBottomSheetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
||||||
with UIMixin {
|
with UIMixin {
|
||||||
final AddEmployeeController _controller = Get.put(AddEmployeeController());
|
late final AddEmployeeController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = Get.put(
|
||||||
|
AddEmployeeController(),
|
||||||
|
tag: UniqueKey().toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (widget.employeeData != null) {
|
||||||
|
_controller.editingEmployeeData = widget.employeeData;
|
||||||
|
_controller.prefillFields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -27,7 +43,7 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
init: _controller,
|
init: _controller,
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
return BaseBottomSheet(
|
return BaseBottomSheet(
|
||||||
title: "Add Employee",
|
title: widget.employeeData != null ? "Edit Employee" : "Add Employee",
|
||||||
onCancel: () => Navigator.pop(context),
|
onCancel: () => Navigator.pop(context),
|
||||||
onSubmit: _handleSubmit,
|
onSubmit: _handleSubmit,
|
||||||
child: Form(
|
child: Form(
|
||||||
@ -98,7 +114,6 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Common label with red star ---
|
|
||||||
Widget _requiredLabel(String text) {
|
Widget _requiredLabel(String text) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
@ -109,7 +124,6 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Date Picker field ---
|
|
||||||
Widget _buildDatePickerField({
|
Widget _buildDatePickerField({
|
||||||
required String label,
|
required String label,
|
||||||
required String value,
|
required String value,
|
||||||
@ -146,7 +160,7 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
Future<void> _pickJoiningDate(BuildContext context) async {
|
Future<void> _pickJoiningDate(BuildContext context) async {
|
||||||
final picked = await showDatePicker(
|
final picked = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
initialDate: DateTime.now(),
|
initialDate: _controller.joiningDate ?? DateTime.now(),
|
||||||
firstDate: DateTime(2000),
|
firstDate: DateTime(2000),
|
||||||
lastDate: DateTime(2100),
|
lastDate: DateTime(2100),
|
||||||
);
|
);
|
||||||
@ -157,54 +171,25 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Submit logic ---
|
|
||||||
Future<void> _handleSubmit() async {
|
Future<void> _handleSubmit() async {
|
||||||
// Run form validation first
|
|
||||||
final isValid =
|
final isValid =
|
||||||
_controller.basicValidator.formKey.currentState?.validate() ?? false;
|
_controller.basicValidator.formKey.currentState?.validate() ?? false;
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid ||
|
||||||
|
_controller.joiningDate == null ||
|
||||||
|
_controller.selectedGender == null ||
|
||||||
|
_controller.selectedRoleId == null) {
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Missing Fields",
|
title: "Missing Fields",
|
||||||
message: "Please fill all required fields before submitting.",
|
message: "Please complete all required fields.",
|
||||||
type: SnackbarType.warning,
|
type: SnackbarType.warning,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional check for dropdowns & joining date
|
final result = await _controller.createOrUpdateEmployee();
|
||||||
if (_controller.joiningDate == null) {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Missing Fields",
|
|
||||||
message: "Please select Joining Date.",
|
|
||||||
type: SnackbarType.warning,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_controller.selectedGender == null) {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Missing Fields",
|
|
||||||
message: "Please select Gender.",
|
|
||||||
type: SnackbarType.warning,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_controller.selectedRoleId == null) {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Missing Fields",
|
|
||||||
message: "Please select Role.",
|
|
||||||
type: SnackbarType.warning,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// All validations passed → Call API
|
|
||||||
final result = await _controller.createEmployees();
|
|
||||||
|
|
||||||
if (result != null && result['success'] == true) {
|
if (result != null && result['success'] == true) {
|
||||||
final employeeData = result['data'];
|
|
||||||
final employeeController = Get.find<EmployeesScreenController>();
|
final employeeController = Get.find<EmployeesScreenController>();
|
||||||
final projectId = employeeController.selectedProjectId;
|
final projectId = employeeController.selectedProjectId;
|
||||||
|
|
||||||
@ -216,20 +201,10 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
|
|
||||||
employeeController.update(['employee_screen_controller']);
|
employeeController.update(['employee_screen_controller']);
|
||||||
|
|
||||||
// Reset form
|
Navigator.pop(context, result['data']);
|
||||||
_controller.basicValidator.getController("first_name")?.clear();
|
|
||||||
_controller.basicValidator.getController("last_name")?.clear();
|
|
||||||
_controller.basicValidator.getController("phone_number")?.clear();
|
|
||||||
_controller.selectedGender = null;
|
|
||||||
_controller.selectedRoleId = null;
|
|
||||||
_controller.joiningDate = null;
|
|
||||||
_controller.update();
|
|
||||||
|
|
||||||
Navigator.pop(context, employeeData);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Section label widget ---
|
|
||||||
Widget _sectionLabel(String title) => Column(
|
Widget _sectionLabel(String title) => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -239,7 +214,6 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- Input field with icon ---
|
|
||||||
Widget _inputWithIcon({
|
Widget _inputWithIcon({
|
||||||
required String label,
|
required String label,
|
||||||
required String hint,
|
required String hint,
|
||||||
@ -268,7 +242,6 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Phone input ---
|
|
||||||
Widget _buildPhoneInput(BuildContext context) {
|
Widget _buildPhoneInput(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -322,7 +295,6 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Dropdown (Gender/Role) ---
|
|
||||||
Widget _buildDropdownField({
|
Widget _buildDropdownField({
|
||||||
required String label,
|
required String label,
|
||||||
required String value,
|
required String value,
|
||||||
@ -356,7 +328,6 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Common input decoration ---
|
|
||||||
InputDecoration _inputDecoration(String hint) {
|
InputDecoration _inputDecoration(String hint) {
|
||||||
return InputDecoration(
|
return InputDecoration(
|
||||||
hintText: hint,
|
hintText: hint,
|
||||||
@ -379,7 +350,6 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Gender popup ---
|
|
||||||
void _showGenderPopup(BuildContext context) async {
|
void _showGenderPopup(BuildContext context) async {
|
||||||
final selected = await showMenu<Gender>(
|
final selected = await showMenu<Gender>(
|
||||||
context: context,
|
context: context,
|
||||||
@ -398,7 +368,6 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Role popup ---
|
|
||||||
void _showRolePopup(BuildContext context) async {
|
void _showRolePopup(BuildContext context) async {
|
||||||
final selected = await showMenu<String>(
|
final selected = await showMenu<String>(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -63,6 +63,7 @@ String _convertDeltaToHtml(dynamic delta) {
|
|||||||
class ContactDetailScreen extends StatefulWidget {
|
class ContactDetailScreen extends StatefulWidget {
|
||||||
final ContactModel contact;
|
final ContactModel contact;
|
||||||
const ContactDetailScreen({super.key, required this.contact});
|
const ContactDetailScreen({super.key, required this.contact});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ContactDetailScreen> createState() => _ContactDetailScreenState();
|
State<ContactDetailScreen> createState() => _ContactDetailScreenState();
|
||||||
}
|
}
|
||||||
@ -70,16 +71,25 @@ class ContactDetailScreen extends StatefulWidget {
|
|||||||
class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||||
late final DirectoryController directoryController;
|
late final DirectoryController directoryController;
|
||||||
late final ProjectController projectController;
|
late final ProjectController projectController;
|
||||||
late ContactModel contact;
|
|
||||||
|
late Rx<ContactModel> contactRx;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
directoryController = Get.find<DirectoryController>();
|
directoryController = Get.find<DirectoryController>();
|
||||||
projectController = Get.find<ProjectController>();
|
projectController = Get.find<ProjectController>();
|
||||||
contact = widget.contact;
|
contactRx = widget.contact.obs;
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
directoryController.fetchCommentsForContact(contact.id);
|
directoryController.fetchCommentsForContact(contactRx.value.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen to controller's allContacts and update contact if changed
|
||||||
|
ever(directoryController.allContacts, (_) {
|
||||||
|
final updated = directoryController.allContacts
|
||||||
|
.firstWhereOrNull((c) => c.id == contactRx.value.id);
|
||||||
|
if (updated != null) contactRx.value = updated;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,12 +104,12 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildSubHeader(),
|
Obx(() => _buildSubHeader(contactRx.value)),
|
||||||
const Divider(height: 1, thickness: 0.5, color: Colors.grey),
|
const Divider(height: 1, thickness: 0.5, color: Colors.grey),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TabBarView(children: [
|
child: TabBarView(children: [
|
||||||
_buildDetailsTab(),
|
Obx(() => _buildDetailsTab(contactRx.value)),
|
||||||
_buildCommentsTab(context),
|
_buildCommentsTab(),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -135,9 +145,9 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
MyText.titleLarge('Contact Profile',
|
MyText.titleLarge('Contact Profile',
|
||||||
fontWeight: 700, color: Colors.black),
|
fontWeight: 700, color: Colors.black),
|
||||||
MySpacing.height(2),
|
MySpacing.height(2),
|
||||||
GetBuilder<ProjectController>(
|
GetBuilder<ProjectController>(builder: (p) {
|
||||||
builder: (p) => ProjectLabel(p.selectedProject?.name),
|
return ProjectLabel(p.selectedProject?.name);
|
||||||
),
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -147,7 +157,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSubHeader() {
|
Widget _buildSubHeader(ContactModel contact) {
|
||||||
final firstName = contact.name.split(" ").first;
|
final firstName = contact.name.split(" ").first;
|
||||||
final lastName =
|
final lastName =
|
||||||
contact.name.split(" ").length > 1 ? contact.name.split(" ").last : "";
|
contact.name.split(" ").length > 1 ? contact.name.split(" ").last : "";
|
||||||
@ -196,7 +206,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDetailsTab() {
|
Widget _buildDetailsTab(ContactModel contact) {
|
||||||
final tags = contact.tags.map((e) => e.name).join(", ");
|
final tags = contact.tags.map((e) => e.name).join(", ");
|
||||||
final bucketNames = contact.bucketIds
|
final bucketNames = contact.bucketIds
|
||||||
.map((id) => directoryController.contactBuckets
|
.map((id) => directoryController.contactBuckets
|
||||||
@ -249,7 +259,6 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MySpacing.height(12),
|
MySpacing.height(12),
|
||||||
// BASIC INFO CARD
|
|
||||||
_infoCard("Basic Info", [
|
_infoCard("Basic Info", [
|
||||||
multiRows(
|
multiRows(
|
||||||
items:
|
items:
|
||||||
@ -273,20 +282,17 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
),
|
),
|
||||||
_iconInfoRow(Icons.location_on, "Address", contact.address),
|
_iconInfoRow(Icons.location_on, "Address", contact.address),
|
||||||
]),
|
]),
|
||||||
// ORGANIZATION CARD
|
|
||||||
_infoCard("Organization", [
|
_infoCard("Organization", [
|
||||||
_iconInfoRow(
|
_iconInfoRow(
|
||||||
Icons.business, "Organization", contact.organization),
|
Icons.business, "Organization", contact.organization),
|
||||||
_iconInfoRow(Icons.category, "Category", category),
|
_iconInfoRow(Icons.category, "Category", category),
|
||||||
]),
|
]),
|
||||||
// META INFO CARD
|
|
||||||
_infoCard("Meta Info", [
|
_infoCard("Meta Info", [
|
||||||
_iconInfoRow(Icons.label, "Tags", tags.isNotEmpty ? tags : "-"),
|
_iconInfoRow(Icons.label, "Tags", tags.isNotEmpty ? tags : "-"),
|
||||||
_iconInfoRow(Icons.folder_shared, "Contact Buckets",
|
_iconInfoRow(Icons.folder_shared, "Contact Buckets",
|
||||||
bucketNames.isNotEmpty ? bucketNames : "-"),
|
bucketNames.isNotEmpty ? bucketNames : "-"),
|
||||||
_iconInfoRow(Icons.work_outline, "Projects", projectNames),
|
_iconInfoRow(Icons.work_outline, "Projects", projectNames),
|
||||||
]),
|
]),
|
||||||
// DESCRIPTION CARD
|
|
||||||
_infoCard("Description", [
|
_infoCard("Description", [
|
||||||
MySpacing.height(6),
|
MySpacing.height(6),
|
||||||
Align(
|
Align(
|
||||||
@ -318,7 +324,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
final updated = directoryController.allContacts
|
final updated = directoryController.allContacts
|
||||||
.firstWhereOrNull((c) => c.id == contact.id);
|
.firstWhereOrNull((c) => c.id == contact.id);
|
||||||
if (updated != null) {
|
if (updated != null) {
|
||||||
setState(() => contact = updated);
|
contactRx.value = updated;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -331,9 +337,9 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCommentsTab(BuildContext context) {
|
Widget _buildCommentsTab() {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
final contactId = contact.id;
|
final contactId = contactRx.value.id;
|
||||||
if (!directoryController.contactCommentsMap.containsKey(contactId)) {
|
if (!directoryController.contactCommentsMap.containsKey(contactId)) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
@ -355,7 +361,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: MediaQuery.of(context).size.height * 0.6,
|
height: Get.height * 0.6,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: MyText.bodyLarge(
|
child: MyText.bodyLarge(
|
||||||
"No comments yet.",
|
"No comments yet.",
|
||||||
@ -375,7 +381,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
itemBuilder: (_, index) => _buildCommentItem(
|
itemBuilder: (_, index) => _buildCommentItem(
|
||||||
comments[index],
|
comments[index],
|
||||||
editingId,
|
editingId,
|
||||||
contact.id,
|
contactId,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -438,7 +444,6 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Header Row
|
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -474,7 +479,6 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// Comment Content
|
|
||||||
if (isEditing && quillController != null)
|
if (isEditing && quillController != null)
|
||||||
CommentEditorCard(
|
CommentEditorCard(
|
||||||
controller: quillController,
|
controller: quillController,
|
||||||
|
@ -591,7 +591,7 @@ class _DirectoryViewState extends State<DirectoryView> {
|
|||||||
FontAwesomeIcons
|
FontAwesomeIcons
|
||||||
.whatsapp,
|
.whatsapp,
|
||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
size: 16,
|
size: 25,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -25,7 +25,8 @@ class DocumentDetailsPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||||
final DocumentDetailsController controller =
|
final DocumentDetailsController controller =
|
||||||
Get.put(DocumentDetailsController());
|
Get.find<DocumentDetailsController>();
|
||||||
|
|
||||||
final PermissionController permissionController =
|
final PermissionController permissionController =
|
||||||
Get.find<PermissionController>();
|
Get.find<PermissionController>();
|
||||||
@override
|
@override
|
||||||
|
@ -17,6 +17,8 @@ import 'package:marco/helpers/widgets/custom_app_bar.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_confirmation_dialog.dart';
|
import 'package:marco/helpers/widgets/my_confirmation_dialog.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/controller/permission_controller.dart';
|
import 'package:marco/controller/permission_controller.dart';
|
||||||
|
import 'package:marco/controller/document/document_details_controller.dart';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
class UserDocumentsPage extends StatefulWidget {
|
class UserDocumentsPage extends StatefulWidget {
|
||||||
final String? entityId;
|
final String? entityId;
|
||||||
@ -36,7 +38,8 @@ class _UserDocumentsPageState extends State<UserDocumentsPage> {
|
|||||||
final DocumentController docController = Get.put(DocumentController());
|
final DocumentController docController = Get.put(DocumentController());
|
||||||
final PermissionController permissionController =
|
final PermissionController permissionController =
|
||||||
Get.find<PermissionController>();
|
Get.find<PermissionController>();
|
||||||
|
final DocumentDetailsController controller =
|
||||||
|
Get.put(DocumentDetailsController());
|
||||||
String get entityTypeId => widget.isEmployee
|
String get entityTypeId => widget.isEmployee
|
||||||
? Permissions.employeeEntity
|
? Permissions.employeeEntity
|
||||||
: Permissions.projectEntity;
|
: Permissions.projectEntity;
|
||||||
@ -504,10 +507,18 @@ class _UserDocumentsPageState extends State<UserDocumentsPage> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: MyRefreshIndicator(
|
child: MyRefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
|
final combinedFilter = {
|
||||||
|
'uploadedByIds': docController.selectedUploadedBy.toList(),
|
||||||
|
'documentCategoryIds':
|
||||||
|
docController.selectedCategory.toList(),
|
||||||
|
'documentTypeIds': docController.selectedType.toList(),
|
||||||
|
'documentTagIds': docController.selectedTag.toList(),
|
||||||
|
};
|
||||||
|
|
||||||
await docController.fetchDocuments(
|
await docController.fetchDocuments(
|
||||||
entityTypeId: entityTypeId,
|
entityTypeId: entityTypeId,
|
||||||
entityId: resolvedEntityId,
|
entityId: resolvedEntityId,
|
||||||
filter: docController.selectedFilter.value,
|
filter: jsonEncode(combinedFilter),
|
||||||
reset: true,
|
reset: true,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -576,8 +587,7 @@ class _UserDocumentsPageState extends State<UserDocumentsPage> {
|
|||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
builder: (_) => DocumentUploadBottomSheet(
|
builder: (_) => DocumentUploadBottomSheet(
|
||||||
isEmployee:
|
isEmployee: widget.isEmployee,
|
||||||
widget.isEmployee, // 👈 Pass the employee flag here
|
|
||||||
onSubmit: (data) async {
|
onSubmit: (data) async {
|
||||||
final success = await uploadController.uploadDocument(
|
final success = await uploadController.uploadDocument(
|
||||||
name: data["name"],
|
name: data["name"],
|
||||||
|
@ -11,6 +11,7 @@ import 'package:marco/helpers/utils/launcher_utils.dart';
|
|||||||
import 'package:marco/controller/permission_controller.dart';
|
import 'package:marco/controller/permission_controller.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';
|
||||||
|
import 'package:marco/model/employees/add_employee_bottom_sheet.dart';
|
||||||
|
|
||||||
class EmployeeDetailPage extends StatefulWidget {
|
class EmployeeDetailPage extends StatefulWidget {
|
||||||
final String employeeId;
|
final String employeeId;
|
||||||
@ -92,8 +93,9 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> {
|
|||||||
fontWeight: FontWeight.normal,
|
fontWeight: FontWeight.normal,
|
||||||
color: (isEmail || isPhone) ? Colors.indigo : Colors.black54,
|
color: (isEmail || isPhone) ? Colors.indigo : Colors.black54,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
decoration:
|
decoration: (isEmail || isPhone)
|
||||||
(isEmail || isPhone) ? TextDecoration.underline : TextDecoration.none,
|
? TextDecoration.underline
|
||||||
|
: TextDecoration.none,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -231,7 +233,7 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> {
|
|||||||
lastName: employee.lastName,
|
lastName: employee.lastName,
|
||||||
size: 45,
|
size: 45,
|
||||||
),
|
),
|
||||||
MySpacing.width(16),
|
MySpacing.width(12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -248,6 +250,34 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit,
|
||||||
|
size: 24, color: Colors.red),
|
||||||
|
onPressed: () async {
|
||||||
|
final result =
|
||||||
|
await showModalBottomSheet<Map<String, dynamic>>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (_) => AddEmployeeBottomSheet(
|
||||||
|
employeeData: {
|
||||||
|
'id': employee.id,
|
||||||
|
'first_name': employee.firstName,
|
||||||
|
'last_name': employee.lastName,
|
||||||
|
'phone_number': employee.phoneNumber,
|
||||||
|
'gender': employee.gender.toLowerCase(),
|
||||||
|
'job_role_id': employee.jobRoleId,
|
||||||
|
'joining_date':
|
||||||
|
employee.joiningDate?.toIso8601String(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
controller.fetchEmployeeDetails(widget.employeeId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
MySpacing.height(14),
|
MySpacing.height(14),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user