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());
// =========================
// 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 {

View File

@ -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())) {

View File

@ -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";

View File

@ -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,
};
}

View File

@ -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);

View File

@ -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,

View File

@ -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,

View File

@ -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,

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/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,

View File

@ -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),

View File

@ -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",

View File

@ -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>
),
);
}
}
}