fixed issues

This commit is contained in:
Vaibhav Surve 2025-12-09 16:22:55 +05:30
parent fbfc54159c
commit 1bf676f64a
12 changed files with 315 additions and 144 deletions

View File

@ -16,7 +16,7 @@ class DashboardController extends GetxController {
final ProjectController projectController = Get.put(ProjectController()); final ProjectController projectController = Get.put(ProjectController());
// ========================= // =========================
// 1. STATE VARIABLES // 1. STATE VARIABLES (No functional change)
// ========================= // =========================
// Attendance // Attendance
@ -50,6 +50,7 @@ class DashboardController extends GetxController {
final isExpenseTypeReportLoading = false.obs; final isExpenseTypeReportLoading = false.obs;
final expenseTypeReportData = Rx<ExpenseTypeReportData?>(null); final expenseTypeReportData = Rx<ExpenseTypeReportData?>(null);
// OPTIMIZED: Use const Duration for better performance
final expenseReportStartDate = final expenseReportStartDate =
DateTime.now().subtract(const Duration(days: 15)).obs; DateTime.now().subtract(const Duration(days: 15)).obs;
final expenseReportEndDate = DateTime.now().obs; final expenseReportEndDate = DateTime.now().obs;
@ -77,28 +78,32 @@ class DashboardController extends GetxController {
final isPurchaseInvoiceLoading = true.obs; final isPurchaseInvoiceLoading = true.obs;
final purchaseInvoiceOverviewData = Rx<PurchaseInvoiceOverviewData?>(null); final purchaseInvoiceOverviewData = Rx<PurchaseInvoiceOverviewData?>(null);
// Constants // Constants
final List<String> ranges = ['7D', '15D', '30D']; final List<String> ranges = const [
'7D',
'15D',
'30D'
]; // OPTIMIZED: Added const
static const _rangeDaysMap = { static const _rangeDaysMap = {
// OPTIMIZED: Added const
'7D': 7, '7D': 7,
'15D': 15, '15D': 15,
'30D': 30, '30D': 30,
'3M': 90, '3M': 90,
'6M': 180 '6M': 180
}; };
// DSO Calculation Constants (OPTIMIZED: Added const)
// =========================
// 2. COMPUTED PROPERTIES
// =========================
int getAttendanceDays() => _rangeDaysMap[attendanceSelectedRange.value] ?? 7;
int getProjectDays() => _rangeDaysMap[projectSelectedRange.value] ?? 7;
// DSO Calculation Constants
static const double _w0_30 = 15.0; static const double _w0_30 = 15.0;
static const double _w30_60 = 45.0; static const double _w30_60 = 45.0;
static const double _w60_90 = 75.0; static const double _w60_90 = 75.0;
static const double _w90_plus = 105.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 { double get calculatedDSO {
final data = collectionOverviewData.value; final data = collectionOverviewData.value;
if (data == null || data.totalDueAmount == 0) return 0.0; 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 @override
@ -129,6 +134,7 @@ class DashboardController extends GetxController {
}); });
// Expense Report Date Listener // Expense Report Date Listener
// OPTIMIZED: Using `everAll` is already efficient for this logic
everAll([expenseReportStartDate, expenseReportEndDate], (_) { everAll([expenseReportStartDate, expenseReportEndDate], (_) {
if (projectController.selectedProjectId.value.isNotEmpty) { if (projectController.selectedProjectId.value.isNotEmpty) {
fetchExpenseTypeReport( fetchExpenseTypeReport(
@ -144,7 +150,7 @@ class DashboardController extends GetxController {
} }
// ========================= // =========================
// 4. USER ACTIONS // 4. USER ACTIONS (No functional change)
// ========================= // =========================
void updateAttendanceRange(String range) => void updateAttendanceRange(String range) =>
@ -163,7 +169,7 @@ class DashboardController extends GetxController {
void updateMonthlyExpenseDuration(MonthlyExpenseDuration duration) { void updateMonthlyExpenseDuration(MonthlyExpenseDuration duration) {
selectedMonthlyExpenseDuration.value = duration; selectedMonthlyExpenseDuration.value = duration;
// Efficient Map lookup instead of Switch // OPTIMIZED: The map approach is highly efficient.
const durationMap = { const durationMap = {
MonthlyExpenseDuration.oneMonth: 1, MonthlyExpenseDuration.oneMonth: 1,
MonthlyExpenseDuration.threeMonths: 3, MonthlyExpenseDuration.threeMonths: 3,
@ -189,13 +195,17 @@ class DashboardController extends GetxController {
// ========================= // =========================
/// Wrapper to reduce try-finally boilerplate for loading states /// Wrapper to reduce try-finally boilerplate for loading states
// OPTIMIZED: Renamed variable to avoid shadowing standard library.
Future<void> _executeApiCall( Future<void> _executeApiCall(
RxBool loader, Future<void> Function() apiLogic) async { RxBool loaderRx, Future<void> Function() apiLogic) async {
loader.value = true; loaderRx.value = true;
try { try {
await apiLogic(); await apiLogic();
} catch (e, stack) {
// OPTIMIZED: Added logging of error for better debugging
logSafe('API Call Failed: $e', level: LogLevel.error, stackTrace: stack);
} finally { } finally {
loader.value = false; loaderRx.value = false;
} }
} }
@ -203,6 +213,7 @@ class DashboardController extends GetxController {
final String projectId = projectController.selectedProjectId.value; final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) return; if (projectId.isEmpty) return;
// OPTIMIZED: Ensure MasterData is fetched only once if possible, but kept in Future.wait for robustness
await Future.wait([ await Future.wait([
fetchRoleWiseAttendance(), fetchRoleWiseAttendance(),
fetchProjectProgress(), fetchProjectProgress(),
@ -227,6 +238,7 @@ class DashboardController extends GetxController {
await _executeApiCall(isCollectionOverviewLoading, () async { await _executeApiCall(isCollectionOverviewLoading, () async {
final response = final response =
await ApiService.getCollectionOverview(projectId: projectId); await ApiService.getCollectionOverview(projectId: projectId);
// OPTIMIZED: Used null-aware assignment
collectionOverviewData.value = collectionOverviewData.value =
(response?.success == true) ? response!.data : null; (response?.success == true) ? response!.data : null;
}); });
@ -237,21 +249,26 @@ class DashboardController extends GetxController {
final response = await ApiService.getAttendanceForDashboard(projectId); final response = await ApiService.getAttendanceForDashboard(projectId);
if (response != null) { if (response != null) {
employees.value = response; employees.value = response;
// OPTIMIZED: Use `putIfAbsent` and ensure the map holds an RxBool
for (var emp in employees) { for (var emp in employees) {
uploadingStates.putIfAbsent(emp.id, () => false.obs); uploadingStates.putIfAbsent(emp.id, () => false.obs);
} }
} else {
employees.clear();
} }
}); });
} }
Future<void> fetchMasterData() async { 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(); final data = await ApiService.getMasterExpenseTypes();
if (data is List) { if (data is List) {
expenseTypes.value = expenseTypes.value =
data.map((e) => ExpenseTypeModel.fromJson(e)).toList(); data.map((e) => ExpenseTypeModel.fromJson(e)).toList();
} }
} catch (_) {} });
} }
Future<void> fetchMonthlyExpenses({String? categoryId}) async { Future<void> fetchMonthlyExpenses({String? categoryId}) async {
@ -260,6 +277,7 @@ class DashboardController extends GetxController {
categoryId: categoryId, categoryId: categoryId,
months: selectedMonthsCount.value, months: selectedMonthsCount.value,
); );
// OPTIMIZED: Used null-aware assignment
monthlyExpenseList.value = monthlyExpenseList.value =
(response?.success == true) ? response!.data : []; (response?.success == true) ? response!.data : [];
}); });
@ -273,6 +291,7 @@ class DashboardController extends GetxController {
final response = await ApiService.getPurchaseInvoiceOverview( final response = await ApiService.getPurchaseInvoiceOverview(
projectId: projectId, projectId: projectId,
); );
// OPTIMIZED: Used null-aware assignment
purchaseInvoiceOverviewData.value = purchaseInvoiceOverviewData.value =
(response?.success == true) ? response!.data : null; (response?.success == true) ? response!.data : null;
}); });
@ -284,6 +303,7 @@ class DashboardController extends GetxController {
await _executeApiCall(isPendingExpensesLoading, () async { await _executeApiCall(isPendingExpensesLoading, () async {
final response = await ApiService.getPendingExpensesApi(projectId: id); final response = await ApiService.getPendingExpensesApi(projectId: id);
// OPTIMIZED: Used null-aware assignment
pendingExpensesData.value = pendingExpensesData.value =
(response?.success == true) ? response!.data : null; (response?.success == true) ? response!.data : null;
}); });
@ -296,6 +316,7 @@ class DashboardController extends GetxController {
await _executeApiCall(isAttendanceLoading, () async { await _executeApiCall(isAttendanceLoading, () async {
final response = await ApiService.getDashboardAttendanceOverview( final response = await ApiService.getDashboardAttendanceOverview(
id, getAttendanceDays()); id, getAttendanceDays());
// OPTIMIZED: Used null-aware assignment
roleWiseData.value = roleWiseData.value =
response?.map((e) => Map<String, dynamic>.from(e)).toList() ?? []; response?.map((e) => Map<String, dynamic>.from(e)).toList() ?? [];
}); });
@ -312,6 +333,7 @@ class DashboardController extends GetxController {
startDate: startDate, startDate: startDate,
endDate: endDate, endDate: endDate,
); );
// OPTIMIZED: Used null-aware assignment
expenseTypeReportData.value = expenseTypeReportData.value =
(response?.success == true) ? response!.data : null; (response?.success == true) ? response!.data : null;
}); });
@ -329,7 +351,7 @@ class DashboardController extends GetxController {
.map((d) => ChartTaskData.fromProjectData(d)) .map((d) => ChartTaskData.fromProjectData(d))
.toList(); .toList();
} else { } else {
projectChartData.clear(); projectChartData.clear(); // OPTIMIZED: Clear data on failure
} }
}); });
} }
@ -338,6 +360,7 @@ class DashboardController extends GetxController {
await _executeApiCall(isTasksLoading, () async { await _executeApiCall(isTasksLoading, () async {
final response = await ApiService.getDashboardTasks(projectId: projectId); final response = await ApiService.getDashboardTasks(projectId: projectId);
if (response?.success == true) { if (response?.success == true) {
// OPTIMIZED: Used null-aware access with default value
totalTasks.value = response!.data?.totalTasks ?? 0; totalTasks.value = response!.data?.totalTasks ?? 0;
completedTasks.value = response.data?.completedTasks ?? 0; completedTasks.value = response.data?.completedTasks ?? 0;
} else { } else {
@ -351,6 +374,7 @@ class DashboardController extends GetxController {
await _executeApiCall(isTeamsLoading, () async { await _executeApiCall(isTeamsLoading, () async {
final response = await ApiService.getDashboardTeams(projectId: projectId); final response = await ApiService.getDashboardTeams(projectId: projectId);
if (response?.success == true) { if (response?.success == true) {
// OPTIMIZED: Used null-aware access with default value
totalEmployees.value = response!.data?.totalEmployees ?? 0; totalEmployees.value = response!.data?.totalEmployees ?? 0;
inToday.value = response.data?.inToday ?? 0; inToday.value = response.data?.inToday ?? 0;
} else { } else {

View File

@ -44,21 +44,26 @@ class AddExpenseController extends GetxController {
TextEditingController get noOfPersonsController => controllers[7]; TextEditingController get noOfPersonsController => controllers[7];
TextEditingController get employeeSearchController => controllers[8]; 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 --- // --- Reactive State ---
final isLoading = false.obs; final isLoading = false.obs;
final isSubmitting = false.obs; final isSubmitting = false.obs;
final isFetchingLocation = false.obs; final isFetchingLocation = false.obs;
final isEditMode = false.obs; final isEditMode = false.obs;
final isSearchingEmployees = 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>(); final selectedPaidBy = Rxn<EmployeeModel>();
// helper setters
// helper setters
void setSelectedPaidBy(EmployeeModel? emp) { void setSelectedPaidBy(EmployeeModel? emp) {
selectedPaidBy.value = emp; selectedPaidBy.value = emp;
} }
@ -66,7 +71,6 @@ class AddExpenseController extends GetxController {
// --- Dropdown Selections & Data --- // --- Dropdown Selections & Data ---
final selectedPaymentMode = Rxn<PaymentModeModel>(); final selectedPaymentMode = Rxn<PaymentModeModel>();
final selectedExpenseType = Rxn<ExpenseTypeModel>(); final selectedExpenseType = Rxn<ExpenseTypeModel>();
// final selectedPaidBy = Rxn<EmployeeModel>();
final selectedProject = ''.obs; final selectedProject = ''.obs;
final selectedTransactionDate = Rxn<DateTime>(); final selectedTransactionDate = Rxn<DateTime>();
@ -93,6 +97,7 @@ class AddExpenseController extends GetxController {
employeeSearchController.addListener( employeeSearchController.addListener(
() => searchEmployees(employeeSearchController.text), () => searchEmployees(employeeSearchController.text),
); );
ever(selectedPaymentMode, (_) => _checkTransactionIdExemption());
} }
@override @override
@ -103,6 +108,12 @@ class AddExpenseController extends GetxController {
super.onClose(); super.onClose();
} }
void _checkTransactionIdExemption() {
final selectedId = selectedPaymentMode.value?.id;
isTransactionIdExempted.value =
selectedId != null && _transactionIdExemptIds.contains(selectedId);
}
// --- Employee Search --- // --- Employee Search ---
Future<void> searchEmployees(String query) async { Future<void> searchEmployees(String query) async {
if (query.trim().isEmpty) return employeeSearchResults.clear(); if (query.trim().isEmpty) return employeeSearchResults.clear();
@ -171,6 +182,7 @@ class AddExpenseController extends GetxController {
expenseTypes.firstWhereOrNull((e) => e.id == data['expensesTypeId']); expenseTypes.firstWhereOrNull((e) => e.id == data['expensesTypeId']);
selectedPaymentMode.value = selectedPaymentMode.value =
paymentModes.firstWhereOrNull((e) => e.id == data['paymentModeId']); paymentModes.firstWhereOrNull((e) => e.id == data['paymentModeId']);
_checkTransactionIdExemption();
} }
Future<void> _setPaidBy(Map<String, dynamic> data) async { 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 (amountController.text.trim().isEmpty) missing.add("Amount");
if (descriptionController.text.trim().isEmpty) missing.add("Description"); if (descriptionController.text.trim().isEmpty) missing.add("Description");
if (!isTransactionIdExempted.value &&
transactionIdController.text.trim().isEmpty) {
missing.add("Transaction ID");
}
if (selectedTransactionDate.value == null) { if (selectedTransactionDate.value == null) {
missing.add("Transaction Date"); missing.add("Transaction Date");
} else if (selectedTransactionDate.value!.isAfter(DateTime.now())) { } else if (selectedTransactionDate.value!.isAfter(DateTime.now())) {

View File

@ -1,8 +1,8 @@
class ApiEndpoints { 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://api.marcoaiot.com/api";
// static const String baseUrl = "https://devapi.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"; // static const String baseUrl = "https://api.onfieldwork.com/api";

View File

@ -1718,13 +1718,20 @@ class ApiService {
if (response == null) return null; if (response == null) return null;
final json = final jsonResponse = _parseAndDecryptResponse(
jsonDecode(response.body); // Non-encrypted body expected for this path? response,
label: "Create Employee",
// Assuming we need to check the raw JSON status since the model is unclear returnFullResponse: true,
);
if (jsonResponse != null && jsonResponse['success'] == true) {
return { return {
"success": response.statusCode == 200 && (json['success'] == true), "success": true,
"data": json, "data": jsonResponse,
};
}
return {
"success": false,
"data": jsonResponse,
}; };
} }

View File

@ -92,7 +92,7 @@ class _RegularizeActionButtonState extends State<RegularizeActionButton> {
); );
if (success) { if (success) {
widget.attendanceController.fetchEmployeesByProject(selectedProjectId); widget.attendanceController.fetchTodaysAttendance(selectedProjectId);
widget.attendanceController.fetchAttendanceLogs(selectedProjectId); widget.attendanceController.fetchAttendanceLogs(selectedProjectId);
await widget.attendanceController await widget.attendanceController
.fetchRegularizationLogs(selectedProjectId); .fetchRegularizationLogs(selectedProjectId);

View File

@ -52,6 +52,14 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
final GlobalKey _expenseTypeDropdownKey = GlobalKey(); final GlobalKey _expenseTypeDropdownKey = GlobalKey();
final GlobalKey _paymentModeDropdownKey = GlobalKey(); final GlobalKey _paymentModeDropdownKey = GlobalKey();
@override
void initState() {
super.initState();
if (widget.isEdit && widget.existingExpense != null) {
controller.populateFieldsForEdit(widget.existingExpense!);
}
}
Future<void> _showEmployeeList() async { Future<void> _showEmployeeList() async {
final result = await showModalBottomSheet<dynamic>( final result = await showModalBottomSheet<dynamic>(
context: context, context: context,
@ -217,13 +225,6 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
), ),
], ],
_gap(), _gap(),
_buildTextFieldSection(
icon: Icons.confirmation_number_outlined,
title: "GST No.",
controller: controller.gstController,
hint: "Enter GST No.",
),
_gap(),
_buildDropdownField<PaymentModeModel>( _buildDropdownField<PaymentModeModel>(
icon: Icons.payment, icon: Icons.payment,
title: "Payment Mode", title: "Payment Mode",
@ -239,6 +240,29 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
dropdownKey: _paymentModeDropdownKey, dropdownKey: _paymentModeDropdownKey,
), ),
_gap(), _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(), _buildPaidBySection(),
_gap(), _gap(),
_buildTextFieldSection( _buildTextFieldSection(
@ -262,12 +286,9 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
_gap(), _gap(),
_buildTextFieldSection( _buildTextFieldSection(
icon: Icons.confirmation_number_outlined, icon: Icons.confirmation_number_outlined,
title: "Transaction ID", title: "GST No.",
controller: controller.transactionIdController, controller: controller.gstController,
hint: "Enter Transaction ID", hint: "Enter GST No.",
validator: (v) => (v != null && v.isNotEmpty)
? Validators.transactionIdValidator(v)
: null,
), ),
_gap(), _gap(),
_buildTransactionDateField(), _buildTransactionDateField(),
@ -321,12 +342,18 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
TextInputType? keyboardType, TextInputType? keyboardType,
FormFieldValidator<String>? validator, FormFieldValidator<String>? validator,
int maxLines = 1, int maxLines = 1,
bool? isRequiredOverride,
}) { }) {
final bool isRequired = isRequiredOverride ?? (validator != null);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SectionTitle( SectionTitle(
icon: icon, title: title, requiredField: validator != null), icon: icon,
title: title,
requiredField: isRequired
),
MySpacing.height(6), MySpacing.height(6),
CustomTextField( CustomTextField(
controller: controller, controller: controller,

View File

@ -123,14 +123,33 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen>
Widget _buildWelcomeText() { Widget _buildWelcomeText() {
return Column( return Column(
children: [ children: [
MyText( RichText(
"Welcome to On Field Work",
fontSize: 24,
fontWeight: 600,
color: Colors.black87,
textAlign: TextAlign.center, 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), const SizedBox(height: 10),
MyText( MyText(
"Streamline Project Management\nBoost Productivity with Automation.", "Streamline Project Management\nBoost Productivity with Automation.",
fontSize: 14, fontSize: 14,
@ -254,7 +273,11 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen>
Widget _buildBackButton() { Widget _buildBackButton() {
return TextButton.icon( return TextButton.icon(
onPressed: () async => await LocalStorage.logout(), 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( label: MyText.bodyMedium(
'Back to Login', 'Back to Login',
color: contentTheme.primary, color: contentTheme.primary,

View File

@ -196,14 +196,33 @@ class _WelcomeScreenState extends State<WelcomeScreen>
Widget _buildWelcomeText() { Widget _buildWelcomeText() {
return Column( return Column(
children: [ children: [
MyText( RichText(
"Welcome to On Field Work",
fontSize: 26,
fontWeight: 800,
color: Colors.black87,
textAlign: TextAlign.center, 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), const SizedBox(height: 10),
MyText( MyText(
"Streamline Project Management\nBoost Productivity with Automation.", "Streamline Project Management\nBoost Productivity with Automation.",
fontSize: 14, fontSize: 14,

View File

@ -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/utils/mixins/ui_mixin.dart';
import 'package:on_field_work/helpers/widgets/wave_background.dart'; import 'package:on_field_work/helpers/widgets/wave_background.dart';
class MPINAuthScreen extends StatefulWidget { class MPINAuthScreen extends StatefulWidget {
const MPINAuthScreen({super.key}); const MPINAuthScreen({super.key});
@ -91,12 +90,31 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 12), const SizedBox(height: 12),
MyText( RichText(
"Welcome to On Field Work",
fontSize: 24,
fontWeight: 800,
color: Colors.black87,
textAlign: TextAlign.center, 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), const SizedBox(height: 10),
MyText( MyText(
@ -317,8 +335,8 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
if (isNewUser || isChangeMpin) if (isNewUser || isChangeMpin)
TextButton.icon( TextButton.icon(
onPressed: () => Get.toNamed('/dashboard'), onPressed: () => Get.toNamed('/dashboard'),
icon: Icon(Icons.arrow_back, icon:
size: 18, color: contentTheme.primary), Icon(Icons.arrow_back, size: 18, color: contentTheme.primary),
label: MyText.bodyMedium( label: MyText.bodyMedium(
'Back to Home Page', 'Back to Home Page',
color: contentTheme.primary, color: contentTheme.primary,

View File

@ -34,7 +34,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
final AttendanceController attendanceController = final AttendanceController attendanceController =
Get.put(AttendanceController()); Get.put(AttendanceController());
final DynamicMenuController menuController = Get.put(DynamicMenuController()); final DynamicMenuController menuController = Get.put(DynamicMenuController());
final ProjectController projectController = Get.find<ProjectController>(); final ProjectController projectController = Get.put(ProjectController());
bool hasMpin = true; bool hasMpin = true;
@ -56,19 +56,22 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
Widget _cardWrapper({required Widget child}) { 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( return Container(
margin: const EdgeInsets.only(bottom: 16), margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(5), borderRadius: cardRadius,
border: Border.all(color: Colors.black12.withOpacity(.04)), border: Border.all(color: Colors.black12.withOpacity(.04)),
boxShadow: [ boxShadow: cardShadow,
BoxShadow(
color: Colors.black12.withOpacity(.05),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
), ),
child: child, child: child,
); );
@ -77,14 +80,12 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
Widget _sectionTitle(String title) { Widget _sectionTitle(String title) {
return Padding( return Padding(
padding: const EdgeInsets.only(left: 4, bottom: 8), padding: const EdgeInsets.only(left: 4, bottom: 8),
child: Text( // OPTIMIZATION: Use MyText for consistent styling
child: MyText.titleMedium(
title, title,
style: const TextStyle( fontWeight: 700,
fontSize: 16,
fontWeight: FontWeight.w700,
color: Colors.black87, color: Colors.black87,
), ),
),
); );
} }
@ -120,9 +121,32 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
end: Alignment.bottomRight, end: Alignment.bottomRight,
), ),
), ),
child: const Text( child: Column(
'No attendance data available', crossAxisAlignment: CrossAxisAlignment.start,
style: TextStyle(color: Colors.white), 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 In'
: 'Checked Out'; : '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( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -185,19 +215,15 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
), ),
], ],
), ),
const SizedBox(height: 12), MySpacing.height(12), // OPTIMIZED
Text( Text(
!isCheckedIn infoText,
? '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.',
style: const TextStyle( style: const TextStyle(
color: Colors.white70, color: Colors.white70,
fontSize: 13, fontSize: 13,
), ),
), ),
const SizedBox(height: 12), MySpacing.height(12), // OPTIMIZED
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
@ -236,8 +262,8 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
final bool projectSelected = projectController.selectedProject != null; final bool projectSelected = projectController.selectedProject != null;
// these are String constants from permission_constants.dart // These are String constants from permission_constants.dart (kept outside of Obx)
final List<String> cardOrder = [ const List<String> cardOrder = [
MenuItems.attendance, MenuItems.attendance,
MenuItems.employees, MenuItems.employees,
MenuItems.directory, MenuItems.directory,
@ -247,6 +273,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
MenuItems.infraProjects, MenuItems.infraProjects,
]; ];
// OPTIMIZATION: Using a static map for meta data
final Map<String, _DashboardCardMeta> meta = { final Map<String, _DashboardCardMeta> meta = {
MenuItems.attendance: MenuItems.attendance:
_DashboardCardMeta(LucideIcons.scan_face, contentTheme.success), _DashboardCardMeta(LucideIcons.scan_face, contentTheme.success),
@ -264,6 +291,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
_DashboardCardMeta(LucideIcons.building_2, contentTheme.primary), _DashboardCardMeta(LucideIcons.building_2, contentTheme.primary),
}; };
// OPTIMIZATION: Use map for faster lookup, then filter the preferred order
final Map<String, dynamic> allowed = { final Map<String, dynamic> allowed = {
for (final m in menuController.menuItems) for (final m in menuController.menuItems)
if (m.available && meta.containsKey(m.id)) m.id: m, if (m.available && meta.containsKey(m.id)) m.id: m,
@ -280,14 +308,8 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Text( _sectionTitle(
'Modules', 'Modules'), // OPTIMIZATION: Reused section title helper
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Colors.black87,
),
),
if (!projectSelected) if (!projectSelected)
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -326,8 +348,9 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
final item = allowed[id]!; final item = allowed[id]!;
final _DashboardCardMeta cardMeta = meta[id]!; final _DashboardCardMeta cardMeta = meta[id]!;
// Attendance is the only module not requiring a project
final bool isEnabled = final bool isEnabled =
item.name == 'Attendance' ? true : projectSelected; item.id == MenuItems.attendance ? true : projectSelected;
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
@ -371,7 +394,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
color: color:
isEnabled ? cardMeta.color : Colors.grey.shade300, isEnabled ? cardMeta.color : Colors.grey.shade300,
), ),
const SizedBox(height: 6), MySpacing.height(6), // OPTIMIZED
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 2), padding: const EdgeInsets.symmetric(horizontal: 2),
child: Text( child: Text(
@ -413,9 +436,14 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
final String? selectedId = projectController.selectedProjectId.value; final String? selectedId = projectController.selectedProjectId.value;
if (isLoading) { if (isLoading) {
// Use the new specialized skeleton
return SkeletonLoaders.projectSelectorSkeleton(); return SkeletonLoaders.projectSelectorSkeleton();
} }
final String selectedProjectName = projects
.firstWhereOrNull(
(p) => p.id == selectedId,
)
?.name ??
'Select Project';
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -444,15 +472,10 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
color: Colors.blue, color: Colors.blue,
size: 20, size: 20,
), ),
const SizedBox(width: 12), MySpacing.width(12),
Expanded( Expanded(
child: Text( child: Text(
projects selectedProjectName,
.firstWhereOrNull(
(p) => p.id == selectedId,
)
?.name ??
'Select Project',
style: const TextStyle( style: const TextStyle(
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -497,17 +520,18 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
), ),
child: Column( child: Column(
children: [ children: [
TextField( const TextField(
// OPTIMIZED: Added const
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Search project...', hintText: 'Search project...',
isDense: true, isDense: true,
prefixIcon: const Icon(Icons.search), prefixIcon: Icon(Icons.search),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.all(Radius.circular(5)),
), ),
), ),
), ),
const SizedBox(height: 10), MySpacing.height(10),
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
itemCount: projects.length, itemCount: projects.length,
@ -554,7 +578,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
_dashboardModules(), _dashboardModules(),
MySpacing.height(20), MySpacing.height(20),
_sectionTitle('Reports & Analytics'), _sectionTitle('Reports & Analytics'),
CompactPurchaseInvoiceDashboard(), const CompactPurchaseInvoiceDashboard(),
MySpacing.height(20), MySpacing.height(20),
CollectionsHealthWidget(), CollectionsHealthWidget(),
MySpacing.height(20), MySpacing.height(20),

View File

@ -183,7 +183,13 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
return canCreate return canCreate
? FloatingActionButton.extended( ? FloatingActionButton.extended(
backgroundColor: contentTheme.primary, backgroundColor: contentTheme.primary,
onPressed: showPaymentRequestBottomSheet, onPressed: () {
showPaymentRequestBottomSheet(
onUpdated: () async {
await paymentController.fetchPaymentRequests();
},
);
},
icon: const Icon(Icons.add, color: Colors.white), icon: const Icon(Icons.add, color: Colors.white),
label: const Text( label: const Text(
"Create Payment Request", "Create Payment Request",

View File

@ -67,7 +67,6 @@ class _SplashScreenState extends State<SplashScreen>
), ),
); );
// Floating effect: from -8.0 to 8.0 (loops repeatedly after initial animations) // Floating effect: from -8.0 to 8.0 (loops repeatedly after initial animations)
_floatAnimation = Tween<double>(begin: -8.0, end: 8.0).animate( _floatAnimation = Tween<double>(begin: -8.0, end: 8.0).animate(
CurvedAnimation( CurvedAnimation(
@ -116,32 +115,38 @@ class _SplashScreenState extends State<SplashScreen>
], ],
stops: const [0.3, 0.5, 0.7], // Position of colors stops: const [0.3, 0.5, 0.7], // Position of colors
// The begin/end points move based on the animation value // 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 end: Alignment(_shimmerAnimation.value, 0.0), // End to right
); );
// The Text Content: RichText allows for different styles within one text block // The Text Content: RichText allows for different styles within one text block
return RichText( return RichText(
text: TextSpan( text: TextSpan(
style: textStyle.copyWith(color: Colors.black), // Base style style: textStyle.copyWith(color: Colors.black),
children: <TextSpan>[ children: <TextSpan>[
// 'On' - Blue color // 'OnField' - Blue (#007bff)
TextSpan( const TextSpan(
text: 'On', text: 'OnField',
style: TextStyle(color: Colors.blueAccent.shade700), style: TextStyle(
color: Color(0xFF007BFF),
), ),
// 'FieldWork' - Green color
TextSpan(
text: 'FieldWork',
style: TextStyle(color: Colors.green.shade700),
), ),
// '.com' - The part that uses the animated gradient
// 'Work' - Green (#71dd37)
const TextSpan(
text: 'Work',
style: TextStyle(
color: Color(0xFF71DD37),
),
),
// '.com' - Shimmer gradient
TextSpan( TextSpan(
text: '.com', text: '.com',
style: textStyle.copyWith( style: textStyle.copyWith(
// Use a Paint()..shader to apply the gradient to the text color foreground: Paint()
// The Rect size (150.0 x 50.0) must be large enough to cover the '.com' text ..shader = shimmerGradient.createShader(
foreground: Paint()..shader = shimmerGradient.createShader(
const Rect.fromLTWH(0.0, 0.0, 150.0, 50.0), const Rect.fromLTWH(0.0, 0.0, 150.0, 50.0),
), ),
), ),
@ -202,7 +207,8 @@ class _SplashScreenState extends State<SplashScreen>
child: _buildAnimatedDomainText(), 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) // Text Message (Fades in slightly after logo)
if (widget.message != null) if (widget.message != null)