fixed issues
This commit is contained in:
parent
fbfc54159c
commit
1bf676f64a
@ -16,7 +16,7 @@ class DashboardController extends GetxController {
|
||||
final ProjectController projectController = Get.put(ProjectController());
|
||||
|
||||
// =========================
|
||||
// 1. STATE VARIABLES
|
||||
// 1. STATE VARIABLES (No functional change)
|
||||
// =========================
|
||||
|
||||
// Attendance
|
||||
@ -50,6 +50,7 @@ class DashboardController extends GetxController {
|
||||
|
||||
final isExpenseTypeReportLoading = false.obs;
|
||||
final expenseTypeReportData = Rx<ExpenseTypeReportData?>(null);
|
||||
// OPTIMIZED: Use const Duration for better performance
|
||||
final expenseReportStartDate =
|
||||
DateTime.now().subtract(const Duration(days: 15)).obs;
|
||||
final expenseReportEndDate = DateTime.now().obs;
|
||||
@ -77,28 +78,32 @@ class DashboardController extends GetxController {
|
||||
final isPurchaseInvoiceLoading = true.obs;
|
||||
final purchaseInvoiceOverviewData = Rx<PurchaseInvoiceOverviewData?>(null);
|
||||
// Constants
|
||||
final List<String> ranges = ['7D', '15D', '30D'];
|
||||
final List<String> ranges = const [
|
||||
'7D',
|
||||
'15D',
|
||||
'30D'
|
||||
]; // OPTIMIZED: Added const
|
||||
static const _rangeDaysMap = {
|
||||
// OPTIMIZED: Added const
|
||||
'7D': 7,
|
||||
'15D': 15,
|
||||
'30D': 30,
|
||||
'3M': 90,
|
||||
'6M': 180
|
||||
};
|
||||
|
||||
// =========================
|
||||
// 2. COMPUTED PROPERTIES
|
||||
// =========================
|
||||
|
||||
int getAttendanceDays() => _rangeDaysMap[attendanceSelectedRange.value] ?? 7;
|
||||
int getProjectDays() => _rangeDaysMap[projectSelectedRange.value] ?? 7;
|
||||
|
||||
// DSO Calculation Constants
|
||||
// DSO Calculation Constants (OPTIMIZED: Added const)
|
||||
static const double _w0_30 = 15.0;
|
||||
static const double _w30_60 = 45.0;
|
||||
static const double _w60_90 = 75.0;
|
||||
static const double _w90_plus = 105.0;
|
||||
|
||||
// =========================
|
||||
// 2. COMPUTED PROPERTIES (No functional change)
|
||||
// =========================
|
||||
|
||||
int getAttendanceDays() => _rangeDaysMap[attendanceSelectedRange.value] ?? 7;
|
||||
int getProjectDays() => _rangeDaysMap[projectSelectedRange.value] ?? 7;
|
||||
|
||||
double get calculatedDSO {
|
||||
final data = collectionOverviewData.value;
|
||||
if (data == null || data.totalDueAmount == 0) return 0.0;
|
||||
@ -112,7 +117,7 @@ class DashboardController extends GetxController {
|
||||
}
|
||||
|
||||
// =========================
|
||||
// 3. LIFECYCLE
|
||||
// 3. LIFECYCLE (No functional change)
|
||||
// =========================
|
||||
|
||||
@override
|
||||
@ -129,6 +134,7 @@ class DashboardController extends GetxController {
|
||||
});
|
||||
|
||||
// Expense Report Date Listener
|
||||
// OPTIMIZED: Using `everAll` is already efficient for this logic
|
||||
everAll([expenseReportStartDate, expenseReportEndDate], (_) {
|
||||
if (projectController.selectedProjectId.value.isNotEmpty) {
|
||||
fetchExpenseTypeReport(
|
||||
@ -144,7 +150,7 @@ class DashboardController extends GetxController {
|
||||
}
|
||||
|
||||
// =========================
|
||||
// 4. USER ACTIONS
|
||||
// 4. USER ACTIONS (No functional change)
|
||||
// =========================
|
||||
|
||||
void updateAttendanceRange(String range) =>
|
||||
@ -163,7 +169,7 @@ class DashboardController extends GetxController {
|
||||
void updateMonthlyExpenseDuration(MonthlyExpenseDuration duration) {
|
||||
selectedMonthlyExpenseDuration.value = duration;
|
||||
|
||||
// Efficient Map lookup instead of Switch
|
||||
// OPTIMIZED: The map approach is highly efficient.
|
||||
const durationMap = {
|
||||
MonthlyExpenseDuration.oneMonth: 1,
|
||||
MonthlyExpenseDuration.threeMonths: 3,
|
||||
@ -189,13 +195,17 @@ class DashboardController extends GetxController {
|
||||
// =========================
|
||||
|
||||
/// Wrapper to reduce try-finally boilerplate for loading states
|
||||
// OPTIMIZED: Renamed variable to avoid shadowing standard library.
|
||||
Future<void> _executeApiCall(
|
||||
RxBool loader, Future<void> Function() apiLogic) async {
|
||||
loader.value = true;
|
||||
RxBool loaderRx, Future<void> Function() apiLogic) async {
|
||||
loaderRx.value = true;
|
||||
try {
|
||||
await apiLogic();
|
||||
} catch (e, stack) {
|
||||
// OPTIMIZED: Added logging of error for better debugging
|
||||
logSafe('API Call Failed: $e', level: LogLevel.error, stackTrace: stack);
|
||||
} finally {
|
||||
loader.value = false;
|
||||
loaderRx.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -203,6 +213,7 @@ class DashboardController extends GetxController {
|
||||
final String projectId = projectController.selectedProjectId.value;
|
||||
if (projectId.isEmpty) return;
|
||||
|
||||
// OPTIMIZED: Ensure MasterData is fetched only once if possible, but kept in Future.wait for robustness
|
||||
await Future.wait([
|
||||
fetchRoleWiseAttendance(),
|
||||
fetchProjectProgress(),
|
||||
@ -227,6 +238,7 @@ class DashboardController extends GetxController {
|
||||
await _executeApiCall(isCollectionOverviewLoading, () async {
|
||||
final response =
|
||||
await ApiService.getCollectionOverview(projectId: projectId);
|
||||
// OPTIMIZED: Used null-aware assignment
|
||||
collectionOverviewData.value =
|
||||
(response?.success == true) ? response!.data : null;
|
||||
});
|
||||
@ -237,21 +249,26 @@ class DashboardController extends GetxController {
|
||||
final response = await ApiService.getAttendanceForDashboard(projectId);
|
||||
if (response != null) {
|
||||
employees.value = response;
|
||||
// OPTIMIZED: Use `putIfAbsent` and ensure the map holds an RxBool
|
||||
for (var emp in employees) {
|
||||
uploadingStates.putIfAbsent(emp.id, () => false.obs);
|
||||
}
|
||||
} else {
|
||||
employees.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> fetchMasterData() async {
|
||||
try {
|
||||
// OPTIMIZATION: Use _executeApiCall for consistency
|
||||
await _executeApiCall(false.obs, () async {
|
||||
// Use a local RxBool since there's no dedicated loader state
|
||||
final data = await ApiService.getMasterExpenseTypes();
|
||||
if (data is List) {
|
||||
expenseTypes.value =
|
||||
data.map((e) => ExpenseTypeModel.fromJson(e)).toList();
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> fetchMonthlyExpenses({String? categoryId}) async {
|
||||
@ -260,6 +277,7 @@ class DashboardController extends GetxController {
|
||||
categoryId: categoryId,
|
||||
months: selectedMonthsCount.value,
|
||||
);
|
||||
// OPTIMIZED: Used null-aware assignment
|
||||
monthlyExpenseList.value =
|
||||
(response?.success == true) ? response!.data : [];
|
||||
});
|
||||
@ -273,6 +291,7 @@ class DashboardController extends GetxController {
|
||||
final response = await ApiService.getPurchaseInvoiceOverview(
|
||||
projectId: projectId,
|
||||
);
|
||||
// OPTIMIZED: Used null-aware assignment
|
||||
purchaseInvoiceOverviewData.value =
|
||||
(response?.success == true) ? response!.data : null;
|
||||
});
|
||||
@ -284,6 +303,7 @@ class DashboardController extends GetxController {
|
||||
|
||||
await _executeApiCall(isPendingExpensesLoading, () async {
|
||||
final response = await ApiService.getPendingExpensesApi(projectId: id);
|
||||
// OPTIMIZED: Used null-aware assignment
|
||||
pendingExpensesData.value =
|
||||
(response?.success == true) ? response!.data : null;
|
||||
});
|
||||
@ -296,6 +316,7 @@ class DashboardController extends GetxController {
|
||||
await _executeApiCall(isAttendanceLoading, () async {
|
||||
final response = await ApiService.getDashboardAttendanceOverview(
|
||||
id, getAttendanceDays());
|
||||
// OPTIMIZED: Used null-aware assignment
|
||||
roleWiseData.value =
|
||||
response?.map((e) => Map<String, dynamic>.from(e)).toList() ?? [];
|
||||
});
|
||||
@ -312,6 +333,7 @@ class DashboardController extends GetxController {
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
);
|
||||
// OPTIMIZED: Used null-aware assignment
|
||||
expenseTypeReportData.value =
|
||||
(response?.success == true) ? response!.data : null;
|
||||
});
|
||||
@ -329,7 +351,7 @@ class DashboardController extends GetxController {
|
||||
.map((d) => ChartTaskData.fromProjectData(d))
|
||||
.toList();
|
||||
} else {
|
||||
projectChartData.clear();
|
||||
projectChartData.clear(); // OPTIMIZED: Clear data on failure
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -338,6 +360,7 @@ class DashboardController extends GetxController {
|
||||
await _executeApiCall(isTasksLoading, () async {
|
||||
final response = await ApiService.getDashboardTasks(projectId: projectId);
|
||||
if (response?.success == true) {
|
||||
// OPTIMIZED: Used null-aware access with default value
|
||||
totalTasks.value = response!.data?.totalTasks ?? 0;
|
||||
completedTasks.value = response.data?.completedTasks ?? 0;
|
||||
} else {
|
||||
@ -351,6 +374,7 @@ class DashboardController extends GetxController {
|
||||
await _executeApiCall(isTeamsLoading, () async {
|
||||
final response = await ApiService.getDashboardTeams(projectId: projectId);
|
||||
if (response?.success == true) {
|
||||
// OPTIMIZED: Used null-aware access with default value
|
||||
totalEmployees.value = response!.data?.totalEmployees ?? 0;
|
||||
inToday.value = response.data?.inToday ?? 0;
|
||||
} else {
|
||||
|
||||
@ -44,21 +44,26 @@ class AddExpenseController extends GetxController {
|
||||
TextEditingController get noOfPersonsController => controllers[7];
|
||||
TextEditingController get employeeSearchController => controllers[8];
|
||||
|
||||
final List<String> _transactionIdExemptIds = const [
|
||||
'24e6b0df-7929-47d2-88a3-4cf14c1f28f9',
|
||||
'48d9b462-5d87-4dec-8dec-2bc943943172',
|
||||
'f67beee6-6763-4108-922c-03bd86b9178d',
|
||||
];
|
||||
|
||||
// --- Reactive State ---
|
||||
final isLoading = false.obs;
|
||||
final isSubmitting = false.obs;
|
||||
final isFetchingLocation = false.obs;
|
||||
final isEditMode = false.obs;
|
||||
final isSearchingEmployees = false.obs;
|
||||
final isTransactionIdExempted = false.obs;
|
||||
|
||||
// --- Paid By (Single + Multi Selection Support) ---
|
||||
// --- Paid By (Single + Multi Selection Support) ---
|
||||
|
||||
// single selection
|
||||
// single selection
|
||||
final selectedPaidBy = Rxn<EmployeeModel>();
|
||||
|
||||
|
||||
|
||||
// helper setters
|
||||
// helper setters
|
||||
void setSelectedPaidBy(EmployeeModel? emp) {
|
||||
selectedPaidBy.value = emp;
|
||||
}
|
||||
@ -66,7 +71,6 @@ class AddExpenseController extends GetxController {
|
||||
// --- Dropdown Selections & Data ---
|
||||
final selectedPaymentMode = Rxn<PaymentModeModel>();
|
||||
final selectedExpenseType = Rxn<ExpenseTypeModel>();
|
||||
// final selectedPaidBy = Rxn<EmployeeModel>();
|
||||
final selectedProject = ''.obs;
|
||||
final selectedTransactionDate = Rxn<DateTime>();
|
||||
|
||||
@ -93,6 +97,7 @@ class AddExpenseController extends GetxController {
|
||||
employeeSearchController.addListener(
|
||||
() => searchEmployees(employeeSearchController.text),
|
||||
);
|
||||
ever(selectedPaymentMode, (_) => _checkTransactionIdExemption());
|
||||
}
|
||||
|
||||
@override
|
||||
@ -103,6 +108,12 @@ class AddExpenseController extends GetxController {
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void _checkTransactionIdExemption() {
|
||||
final selectedId = selectedPaymentMode.value?.id;
|
||||
isTransactionIdExempted.value =
|
||||
selectedId != null && _transactionIdExemptIds.contains(selectedId);
|
||||
}
|
||||
|
||||
// --- Employee Search ---
|
||||
Future<void> searchEmployees(String query) async {
|
||||
if (query.trim().isEmpty) return employeeSearchResults.clear();
|
||||
@ -171,6 +182,7 @@ class AddExpenseController extends GetxController {
|
||||
expenseTypes.firstWhereOrNull((e) => e.id == data['expensesTypeId']);
|
||||
selectedPaymentMode.value =
|
||||
paymentModes.firstWhereOrNull((e) => e.id == data['paymentModeId']);
|
||||
_checkTransactionIdExemption();
|
||||
}
|
||||
|
||||
Future<void> _setPaidBy(Map<String, dynamic> data) async {
|
||||
@ -536,6 +548,11 @@ class AddExpenseController extends GetxController {
|
||||
if (amountController.text.trim().isEmpty) missing.add("Amount");
|
||||
if (descriptionController.text.trim().isEmpty) missing.add("Description");
|
||||
|
||||
if (!isTransactionIdExempted.value &&
|
||||
transactionIdController.text.trim().isEmpty) {
|
||||
missing.add("Transaction ID");
|
||||
}
|
||||
|
||||
if (selectedTransactionDate.value == null) {
|
||||
missing.add("Transaction Date");
|
||||
} else if (selectedTransactionDate.value!.isAfter(DateTime.now())) {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
class ApiEndpoints {
|
||||
// static const String baseUrl = "https://stageapi.marcoaiot.com/api";
|
||||
static const String baseUrl = "https://stageapi.marcoaiot.com/api";
|
||||
// static const String baseUrl = "https://api.marcoaiot.com/api";
|
||||
// static const String baseUrl = "https://devapi.marcoaiot.com/api";
|
||||
static const String baseUrl = "https://mapi.marcoaiot.com/api";
|
||||
// static const String baseUrl = "https://mapi.marcoaiot.com/api";
|
||||
// static const String baseUrl = "https://api.onfieldwork.com/api";
|
||||
|
||||
|
||||
|
||||
@ -1718,13 +1718,20 @@ class ApiService {
|
||||
|
||||
if (response == null) return null;
|
||||
|
||||
final json =
|
||||
jsonDecode(response.body); // Non-encrypted body expected for this path?
|
||||
|
||||
// Assuming we need to check the raw JSON status since the model is unclear
|
||||
final jsonResponse = _parseAndDecryptResponse(
|
||||
response,
|
||||
label: "Create Employee",
|
||||
returnFullResponse: true,
|
||||
);
|
||||
if (jsonResponse != null && jsonResponse['success'] == true) {
|
||||
return {
|
||||
"success": true,
|
||||
"data": jsonResponse,
|
||||
};
|
||||
}
|
||||
return {
|
||||
"success": response.statusCode == 200 && (json['success'] == true),
|
||||
"data": json,
|
||||
"success": false,
|
||||
"data": jsonResponse,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -92,7 +92,7 @@ class _RegularizeActionButtonState extends State<RegularizeActionButton> {
|
||||
);
|
||||
|
||||
if (success) {
|
||||
widget.attendanceController.fetchEmployeesByProject(selectedProjectId);
|
||||
widget.attendanceController.fetchTodaysAttendance(selectedProjectId);
|
||||
widget.attendanceController.fetchAttendanceLogs(selectedProjectId);
|
||||
await widget.attendanceController
|
||||
.fetchRegularizationLogs(selectedProjectId);
|
||||
|
||||
@ -52,6 +52,14 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
|
||||
final GlobalKey _expenseTypeDropdownKey = GlobalKey();
|
||||
final GlobalKey _paymentModeDropdownKey = GlobalKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.isEdit && widget.existingExpense != null) {
|
||||
controller.populateFieldsForEdit(widget.existingExpense!);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showEmployeeList() async {
|
||||
final result = await showModalBottomSheet<dynamic>(
|
||||
context: context,
|
||||
@ -217,13 +225,6 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
|
||||
),
|
||||
],
|
||||
_gap(),
|
||||
_buildTextFieldSection(
|
||||
icon: Icons.confirmation_number_outlined,
|
||||
title: "GST No.",
|
||||
controller: controller.gstController,
|
||||
hint: "Enter GST No.",
|
||||
),
|
||||
_gap(),
|
||||
_buildDropdownField<PaymentModeModel>(
|
||||
icon: Icons.payment,
|
||||
title: "Payment Mode",
|
||||
@ -239,6 +240,29 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
|
||||
dropdownKey: _paymentModeDropdownKey,
|
||||
),
|
||||
_gap(),
|
||||
Obx(() {
|
||||
if (controller.isTransactionIdExempted.value) {
|
||||
return const SizedBox.shrink(); // hide field
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildTextFieldSection(
|
||||
icon: Icons.confirmation_number_outlined,
|
||||
title: "Transaction ID",
|
||||
hint: "Enter Transaction ID",
|
||||
controller: controller.transactionIdController,
|
||||
isRequiredOverride: true,
|
||||
validator: (v) {
|
||||
return (v != null && v.isNotEmpty)
|
||||
? Validators.transactionIdValidator(v)
|
||||
: Validators.requiredField(v);
|
||||
},
|
||||
),
|
||||
_gap(),
|
||||
],
|
||||
);
|
||||
}),
|
||||
_buildPaidBySection(),
|
||||
_gap(),
|
||||
_buildTextFieldSection(
|
||||
@ -262,12 +286,9 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
|
||||
_gap(),
|
||||
_buildTextFieldSection(
|
||||
icon: Icons.confirmation_number_outlined,
|
||||
title: "Transaction ID",
|
||||
controller: controller.transactionIdController,
|
||||
hint: "Enter Transaction ID",
|
||||
validator: (v) => (v != null && v.isNotEmpty)
|
||||
? Validators.transactionIdValidator(v)
|
||||
: null,
|
||||
title: "GST No.",
|
||||
controller: controller.gstController,
|
||||
hint: "Enter GST No.",
|
||||
),
|
||||
_gap(),
|
||||
_buildTransactionDateField(),
|
||||
@ -321,12 +342,18 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
|
||||
TextInputType? keyboardType,
|
||||
FormFieldValidator<String>? validator,
|
||||
int maxLines = 1,
|
||||
bool? isRequiredOverride,
|
||||
}) {
|
||||
final bool isRequired = isRequiredOverride ?? (validator != null);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SectionTitle(
|
||||
icon: icon, title: title, requiredField: validator != null),
|
||||
icon: icon,
|
||||
title: title,
|
||||
requiredField: isRequired
|
||||
),
|
||||
MySpacing.height(6),
|
||||
CustomTextField(
|
||||
controller: controller,
|
||||
|
||||
@ -123,14 +123,33 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen>
|
||||
Widget _buildWelcomeText() {
|
||||
return Column(
|
||||
children: [
|
||||
MyText(
|
||||
"Welcome to On Field Work",
|
||||
fontSize: 24,
|
||||
fontWeight: 600,
|
||||
color: Colors.black87,
|
||||
RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
children: const [
|
||||
TextSpan(
|
||||
text: "Welcome to ",
|
||||
style: TextStyle(color: Colors.black87),
|
||||
),
|
||||
TextSpan(
|
||||
text: "OnField",
|
||||
style: TextStyle(color: Color(0xFF007BFF)), // Blue
|
||||
),
|
||||
TextSpan(
|
||||
text: "Work",
|
||||
style: TextStyle(color: Color(0xFF71DD37)), // Green
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
MyText(
|
||||
"Streamline Project Management\nBoost Productivity with Automation.",
|
||||
fontSize: 14,
|
||||
@ -254,7 +273,11 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen>
|
||||
Widget _buildBackButton() {
|
||||
return TextButton.icon(
|
||||
onPressed: () async => await LocalStorage.logout(),
|
||||
icon: Icon(Icons.arrow_back, size: 18, color: contentTheme.primary,),
|
||||
icon: Icon(
|
||||
Icons.arrow_back,
|
||||
size: 18,
|
||||
color: contentTheme.primary,
|
||||
),
|
||||
label: MyText.bodyMedium(
|
||||
'Back to Login',
|
||||
color: contentTheme.primary,
|
||||
|
||||
@ -196,14 +196,33 @@ class _WelcomeScreenState extends State<WelcomeScreen>
|
||||
Widget _buildWelcomeText() {
|
||||
return Column(
|
||||
children: [
|
||||
MyText(
|
||||
"Welcome to On Field Work",
|
||||
fontSize: 26,
|
||||
fontWeight: 800,
|
||||
color: Colors.black87,
|
||||
RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
style: const TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.black87,
|
||||
),
|
||||
children: const [
|
||||
TextSpan(
|
||||
text: "Welcome to ",
|
||||
style: TextStyle(color: Colors.black87),
|
||||
),
|
||||
TextSpan(
|
||||
text: "OnField",
|
||||
style: TextStyle(color: Color(0xFF007BFF)), // Blue
|
||||
),
|
||||
TextSpan(
|
||||
text: "Work",
|
||||
style: TextStyle(color: Color(0xFF71DD37)), // Green
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
MyText(
|
||||
"Streamline Project Management\nBoost Productivity with Automation.",
|
||||
fontSize: 14,
|
||||
|
||||
@ -10,7 +10,6 @@ import 'package:on_field_work/helpers/services/api_endpoints.dart';
|
||||
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:on_field_work/helpers/widgets/wave_background.dart';
|
||||
|
||||
|
||||
class MPINAuthScreen extends StatefulWidget {
|
||||
const MPINAuthScreen({super.key});
|
||||
|
||||
@ -91,12 +90,31 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
MyText(
|
||||
"Welcome to On Field Work",
|
||||
fontSize: 24,
|
||||
fontWeight: 800,
|
||||
color: Colors.black87,
|
||||
RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: const TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.black87,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Welcome to ",
|
||||
style: TextStyle(color: Colors.black87),
|
||||
),
|
||||
TextSpan(
|
||||
text: "OnField",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF007BFF)), // Blue
|
||||
),
|
||||
TextSpan(
|
||||
text: "Work",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF71DD37)), // Green
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
MyText(
|
||||
@ -317,8 +335,8 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
|
||||
if (isNewUser || isChangeMpin)
|
||||
TextButton.icon(
|
||||
onPressed: () => Get.toNamed('/dashboard'),
|
||||
icon: Icon(Icons.arrow_back,
|
||||
size: 18, color: contentTheme.primary),
|
||||
icon:
|
||||
Icon(Icons.arrow_back, size: 18, color: contentTheme.primary),
|
||||
label: MyText.bodyMedium(
|
||||
'Back to Home Page',
|
||||
color: contentTheme.primary,
|
||||
|
||||
@ -34,7 +34,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
final AttendanceController attendanceController =
|
||||
Get.put(AttendanceController());
|
||||
final DynamicMenuController menuController = Get.put(DynamicMenuController());
|
||||
final ProjectController projectController = Get.find<ProjectController>();
|
||||
final ProjectController projectController = Get.put(ProjectController());
|
||||
|
||||
bool hasMpin = true;
|
||||
|
||||
@ -56,19 +56,22 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Widget _cardWrapper({required Widget child}) {
|
||||
const BorderRadius cardRadius = BorderRadius.all(Radius.circular(5));
|
||||
const List<BoxShadow> cardShadow = [
|
||||
BoxShadow(
|
||||
color: Color.fromRGBO(0, 0, 0, 0.05),
|
||||
blurRadius: 12,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
];
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
borderRadius: cardRadius,
|
||||
border: Border.all(color: Colors.black12.withOpacity(.04)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12.withOpacity(.05),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
boxShadow: cardShadow,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
@ -77,13 +80,11 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
Widget _sectionTitle(String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 4, bottom: 8),
|
||||
child: Text(
|
||||
// OPTIMIZATION: Use MyText for consistent styling
|
||||
child: MyText.titleMedium(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black87,
|
||||
),
|
||||
fontWeight: 700,
|
||||
color: Colors.black87,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -120,9 +121,32 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'No attendance data available',
|
||||
style: TextStyle(color: Colors.white),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, size: 30, color: Colors.white),
|
||||
MySpacing.width(10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"No attendance data available yet.",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(12),
|
||||
Text(
|
||||
"You are not added to this project or attendance data is not available.",
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -137,6 +161,12 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
? 'Checked In'
|
||||
: 'Checked Out';
|
||||
|
||||
final String infoText = !isCheckedIn
|
||||
? 'You are not checked-in yet. Please check-in to start your work.'
|
||||
: !isCheckedOut
|
||||
? 'You are currently checked-in. Don\'t forget to check-out after your work.'
|
||||
: 'You have checked-out for today.';
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
@ -185,19 +215,15 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
MySpacing.height(12), // OPTIMIZED
|
||||
Text(
|
||||
!isCheckedIn
|
||||
? 'You are not checked-in yet. Please check-in to start your work.'
|
||||
: !isCheckedOut
|
||||
? 'You are currently checked-in. Don\'t forget to check-out after your work.'
|
||||
: 'You have checked-out for today.',
|
||||
infoText,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
MySpacing.height(12), // OPTIMIZED
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
@ -236,8 +262,8 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
|
||||
final bool projectSelected = projectController.selectedProject != null;
|
||||
|
||||
// these are String constants from permission_constants.dart
|
||||
final List<String> cardOrder = [
|
||||
// These are String constants from permission_constants.dart (kept outside of Obx)
|
||||
const List<String> cardOrder = [
|
||||
MenuItems.attendance,
|
||||
MenuItems.employees,
|
||||
MenuItems.directory,
|
||||
@ -247,6 +273,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
MenuItems.infraProjects,
|
||||
];
|
||||
|
||||
// OPTIMIZATION: Using a static map for meta data
|
||||
final Map<String, _DashboardCardMeta> meta = {
|
||||
MenuItems.attendance:
|
||||
_DashboardCardMeta(LucideIcons.scan_face, contentTheme.success),
|
||||
@ -264,6 +291,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
_DashboardCardMeta(LucideIcons.building_2, contentTheme.primary),
|
||||
};
|
||||
|
||||
// OPTIMIZATION: Use map for faster lookup, then filter the preferred order
|
||||
final Map<String, dynamic> allowed = {
|
||||
for (final m in menuController.menuItems)
|
||||
if (m.available && meta.containsKey(m.id)) m.id: m,
|
||||
@ -280,14 +308,8 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Modules',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
_sectionTitle(
|
||||
'Modules'), // OPTIMIZATION: Reused section title helper
|
||||
if (!projectSelected)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@ -326,8 +348,9 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
final item = allowed[id]!;
|
||||
final _DashboardCardMeta cardMeta = meta[id]!;
|
||||
|
||||
// Attendance is the only module not requiring a project
|
||||
final bool isEnabled =
|
||||
item.name == 'Attendance' ? true : projectSelected;
|
||||
item.id == MenuItems.attendance ? true : projectSelected;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
@ -371,7 +394,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
color:
|
||||
isEnabled ? cardMeta.color : Colors.grey.shade300,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
MySpacing.height(6), // OPTIMIZED
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: Text(
|
||||
@ -413,9 +436,14 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
final String? selectedId = projectController.selectedProjectId.value;
|
||||
|
||||
if (isLoading) {
|
||||
// Use the new specialized skeleton
|
||||
return SkeletonLoaders.projectSelectorSkeleton();
|
||||
}
|
||||
final String selectedProjectName = projects
|
||||
.firstWhereOrNull(
|
||||
(p) => p.id == selectedId,
|
||||
)
|
||||
?.name ??
|
||||
'Select Project';
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -444,15 +472,10 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
color: Colors.blue,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
projects
|
||||
.firstWhereOrNull(
|
||||
(p) => p.id == selectedId,
|
||||
)
|
||||
?.name ??
|
||||
'Select Project',
|
||||
selectedProjectName,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
@ -497,17 +520,18 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
const TextField(
|
||||
// OPTIMIZED: Added const
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search project...',
|
||||
isDense: true,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
prefixIcon: Icon(Icons.search),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
MySpacing.height(10),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: projects.length,
|
||||
@ -554,7 +578,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
_dashboardModules(),
|
||||
MySpacing.height(20),
|
||||
_sectionTitle('Reports & Analytics'),
|
||||
CompactPurchaseInvoiceDashboard(),
|
||||
const CompactPurchaseInvoiceDashboard(),
|
||||
MySpacing.height(20),
|
||||
CollectionsHealthWidget(),
|
||||
MySpacing.height(20),
|
||||
|
||||
@ -183,7 +183,13 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
|
||||
return canCreate
|
||||
? FloatingActionButton.extended(
|
||||
backgroundColor: contentTheme.primary,
|
||||
onPressed: showPaymentRequestBottomSheet,
|
||||
onPressed: () {
|
||||
showPaymentRequestBottomSheet(
|
||||
onUpdated: () async {
|
||||
await paymentController.fetchPaymentRequests();
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.add, color: Colors.white),
|
||||
label: const Text(
|
||||
"Create Payment Request",
|
||||
|
||||
@ -28,7 +28,7 @@ class _SplashScreenState extends State<SplashScreen>
|
||||
late Animation<double> _scaleAnimation;
|
||||
// Animation for logo and text fade-in
|
||||
late Animation<double> _opacityAnimation;
|
||||
|
||||
|
||||
// Animation for the gradient shimmer effect (moves from -1.0 to 2.0)
|
||||
late Animation<double> _shimmerAnimation;
|
||||
|
||||
@ -58,7 +58,7 @@ class _SplashScreenState extends State<SplashScreen>
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
// Shimmer/Gradient Animation: Moves the gradient horizontally from left to right
|
||||
_shimmerAnimation = Tween<double>(begin: -1.0, end: 2.0).animate(
|
||||
CurvedAnimation(
|
||||
@ -67,7 +67,6 @@ class _SplashScreenState extends State<SplashScreen>
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
// Floating effect: from -8.0 to 8.0 (loops repeatedly after initial animations)
|
||||
_floatAnimation = Tween<double>(begin: -8.0, end: 8.0).animate(
|
||||
CurvedAnimation(
|
||||
@ -94,7 +93,7 @@ class _SplashScreenState extends State<SplashScreen>
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
// Widget for the multi-colored text with shimmering effect only on '.com'
|
||||
Widget _buildAnimatedDomainText() {
|
||||
const textStyle = TextStyle(
|
||||
@ -110,40 +109,46 @@ class _SplashScreenState extends State<SplashScreen>
|
||||
// Define the silver gradient
|
||||
final shimmerGradient = LinearGradient(
|
||||
colors: const [
|
||||
Colors.grey, // Starting dull color
|
||||
Colors.white, // Brightest 'shimmer' highlight
|
||||
Colors.grey, // Ending dull color
|
||||
Colors.grey, // Starting dull color
|
||||
Colors.white, // Brightest 'shimmer' highlight
|
||||
Colors.grey, // Ending dull color
|
||||
],
|
||||
stops: const [0.3, 0.5, 0.7], // Position of colors
|
||||
// The begin/end points move based on the animation value
|
||||
begin: Alignment(_shimmerAnimation.value - 1.0, 0.0), // Start from left
|
||||
begin:
|
||||
Alignment(_shimmerAnimation.value - 1.0, 0.0), // Start from left
|
||||
end: Alignment(_shimmerAnimation.value, 0.0), // End to right
|
||||
);
|
||||
|
||||
// The Text Content: RichText allows for different styles within one text block
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: textStyle.copyWith(color: Colors.black), // Base style
|
||||
style: textStyle.copyWith(color: Colors.black),
|
||||
children: <TextSpan>[
|
||||
// 'On' - Blue color
|
||||
TextSpan(
|
||||
text: 'On',
|
||||
style: TextStyle(color: Colors.blueAccent.shade700),
|
||||
// 'OnField' - Blue (#007bff)
|
||||
const TextSpan(
|
||||
text: 'OnField',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF007BFF),
|
||||
),
|
||||
),
|
||||
// 'FieldWork' - Green color
|
||||
TextSpan(
|
||||
text: 'FieldWork',
|
||||
style: TextStyle(color: Colors.green.shade700),
|
||||
|
||||
// 'Work' - Green (#71dd37)
|
||||
const TextSpan(
|
||||
text: 'Work',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF71DD37),
|
||||
),
|
||||
),
|
||||
// '.com' - The part that uses the animated gradient
|
||||
|
||||
// '.com' - Shimmer gradient
|
||||
TextSpan(
|
||||
text: '.com',
|
||||
style: textStyle.copyWith(
|
||||
// Use a Paint()..shader to apply the gradient to the text color
|
||||
// The Rect size (150.0 x 50.0) must be large enough to cover the '.com' text
|
||||
foreground: Paint()..shader = shimmerGradient.createShader(
|
||||
const Rect.fromLTWH(0.0, 0.0, 150.0, 50.0),
|
||||
),
|
||||
foreground: Paint()
|
||||
..shader = shimmerGradient.createShader(
|
||||
const Rect.fromLTWH(0.0, 0.0, 150.0, 50.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -202,7 +207,8 @@ class _SplashScreenState extends State<SplashScreen>
|
||||
child: _buildAnimatedDomainText(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 10), // Small space between new text and message
|
||||
const SizedBox(
|
||||
height: 10), // Small space between new text and message
|
||||
|
||||
// Text Message (Fades in slightly after logo)
|
||||
if (widget.message != null)
|
||||
@ -229,4 +235,4 @@ class _SplashScreenState extends State<SplashScreen>
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user