added safe area to support mobile screen horizontally
This commit is contained in:
parent
5bed5bd2f4
commit
24bfccfdf6
@ -123,7 +123,6 @@ class _AttendanceFilterBottomSheetState
|
|||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
final List<Widget> widgets = [
|
final List<Widget> widgets = [
|
||||||
// 🔹 View Section
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 4),
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
child: Align(
|
child: Align(
|
||||||
@ -146,7 +145,6 @@ class _AttendanceFilterBottomSheetState
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
// 🔹 Organization filter
|
|
||||||
widgets.addAll([
|
widgets.addAll([
|
||||||
const Divider(),
|
const Divider(),
|
||||||
Padding(
|
Padding(
|
||||||
@ -165,24 +163,6 @@ class _AttendanceFilterBottomSheetState
|
|||||||
color: Colors.grey.shade300,
|
color: Colors.grey.shade300,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 100,
|
|
||||||
height: 14,
|
|
||||||
color: Colors.grey.shade400,
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: 18,
|
|
||||||
height: 18,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade400,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} else if (widget.controller.organizations.isEmpty) {
|
} else if (widget.controller.organizations.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
@ -200,7 +180,6 @@ class _AttendanceFilterBottomSheetState
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 🔹 Date Range (only for Attendance Logs)
|
|
||||||
if (tempSelectedTab == 'attendanceLogs') {
|
if (tempSelectedTab == 'attendanceLogs') {
|
||||||
widgets.addAll([
|
widgets.addAll([
|
||||||
const Divider(),
|
const Divider(),
|
||||||
@ -211,14 +190,12 @@ class _AttendanceFilterBottomSheetState
|
|||||||
child: MyText.titleSmall("Date Range", fontWeight: 600),
|
child: MyText.titleSmall("Date Range", fontWeight: 600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// ✅ Reusable DateRangePickerWidget
|
|
||||||
DateRangePickerWidget(
|
DateRangePickerWidget(
|
||||||
startDate: widget.controller.startDateAttendance,
|
startDate: widget.controller.startDateAttendance,
|
||||||
endDate: widget.controller.endDateAttendance,
|
endDate: widget.controller.endDateAttendance,
|
||||||
startLabel: "Start Date",
|
startLabel: "Start Date",
|
||||||
endLabel: "End Date",
|
endLabel: "End Date",
|
||||||
onDateRangeSelected: (start, end) {
|
onDateRangeSelected: (start, end) {
|
||||||
// Optional: trigger UI updates if needed
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -230,8 +207,8 @@ class _AttendanceFilterBottomSheetState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ClipRRect(
|
return SafeArea(
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
// ← FIX: avoids hiding under navigation buttons
|
||||||
child: BaseBottomSheet(
|
child: BaseBottomSheet(
|
||||||
title: "Attendance Filter",
|
title: "Attendance Filter",
|
||||||
submitText: "Apply",
|
submitText: "Apply",
|
||||||
@ -240,9 +217,17 @@ class _AttendanceFilterBottomSheetState
|
|||||||
'selectedTab': tempSelectedTab,
|
'selectedTab': tempSelectedTab,
|
||||||
'selectedOrganization': widget.controller.selectedOrganization?.id,
|
'selectedOrganization': widget.controller.selectedOrganization?.id,
|
||||||
}),
|
}),
|
||||||
child: Column(
|
child: Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding:
|
||||||
children: buildMainFilters(),
|
const EdgeInsets.only(bottom: 24), // ← FIX: extra safe padding
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
// ← FIX: full scrollable in landscape
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: buildMainFilters(),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import 'package:on_field_work/controller/task_planning/daily_task_controller.dart';
|
import 'package:on_field_work/controller/task_planning/daily_task_controller.dart';
|
||||||
import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart';
|
import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
||||||
@ -23,82 +24,85 @@ class DailyTaskFilterBottomSheet extends StatelessWidget {
|
|||||||
filterData.services,
|
filterData.services,
|
||||||
].any((list) => list.isNotEmpty);
|
].any((list) => list.isNotEmpty);
|
||||||
|
|
||||||
return BaseBottomSheet(
|
return SafeArea(
|
||||||
title: "Filter Tasks",
|
// ✅ PREVENTS GOING UNDER NAV BUTTONS
|
||||||
submitText: "Apply",
|
bottom: true,
|
||||||
showButtons: hasFilters,
|
child: BaseBottomSheet(
|
||||||
onCancel: () => Get.back(),
|
title: "Filter Tasks",
|
||||||
onSubmit: () {
|
submitText: "Apply",
|
||||||
if (controller.selectedProjectId != null) {
|
showButtons: hasFilters,
|
||||||
controller.fetchTaskData(
|
onCancel: () => Get.back(),
|
||||||
controller.selectedProjectId!,
|
onSubmit: () {
|
||||||
);
|
if (controller.selectedProjectId != null) {
|
||||||
}
|
controller.fetchTaskData(controller.selectedProjectId!);
|
||||||
|
}
|
||||||
Get.back();
|
Get.back();
|
||||||
},
|
},
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: hasFilters
|
padding: const EdgeInsets.only(bottom: 40), // ✅ EXTRA SAFETY PADDING
|
||||||
? Column(
|
child: hasFilters
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
? Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Align(
|
children: [
|
||||||
alignment: Alignment.centerRight,
|
Align(
|
||||||
child: TextButton(
|
alignment: Alignment.centerRight,
|
||||||
onPressed: () {
|
child: TextButton(
|
||||||
controller.clearTaskFilters();
|
onPressed: () {
|
||||||
},
|
controller.clearTaskFilters();
|
||||||
child: MyText(
|
},
|
||||||
"Reset Filter",
|
child: MyText(
|
||||||
style: const TextStyle(
|
"Reset Filter",
|
||||||
color: Colors.red,
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
color: Colors.red,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
MySpacing.height(8),
|
||||||
MySpacing.height(8),
|
_multiSelectField(
|
||||||
_multiSelectField(
|
label: "Buildings",
|
||||||
label: "Buildings",
|
items: filterData.buildings,
|
||||||
items: filterData.buildings,
|
fallback: "Select Buildings",
|
||||||
fallback: "Select Buildings",
|
selectedValues: controller.selectedBuildings,
|
||||||
selectedValues: controller.selectedBuildings,
|
),
|
||||||
),
|
_multiSelectField(
|
||||||
_multiSelectField(
|
label: "Floors",
|
||||||
label: "Floors",
|
items: filterData.floors,
|
||||||
items: filterData.floors,
|
fallback: "Select Floors",
|
||||||
fallback: "Select Floors",
|
selectedValues: controller.selectedFloors,
|
||||||
selectedValues: controller.selectedFloors,
|
),
|
||||||
),
|
_multiSelectField(
|
||||||
_multiSelectField(
|
label: "Activities",
|
||||||
label: "Activities",
|
items: filterData.activities,
|
||||||
items: filterData.activities,
|
fallback: "Select Activities",
|
||||||
fallback: "Select Activities",
|
selectedValues: controller.selectedActivities,
|
||||||
selectedValues: controller.selectedActivities,
|
),
|
||||||
),
|
_multiSelectField(
|
||||||
_multiSelectField(
|
label: "Services",
|
||||||
label: "Services",
|
items: filterData.services,
|
||||||
items: filterData.services,
|
fallback: "Select Services",
|
||||||
fallback: "Select Services",
|
selectedValues: controller.selectedServices,
|
||||||
selectedValues: controller.selectedServices,
|
),
|
||||||
),
|
MySpacing.height(8),
|
||||||
MySpacing.height(8),
|
_dateRangeSelector(context),
|
||||||
_dateRangeSelector(context),
|
],
|
||||||
],
|
)
|
||||||
)
|
: Center(
|
||||||
: Center(
|
child: Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.all(24.0),
|
||||||
padding: const EdgeInsets.all(24.0),
|
child: MyText(
|
||||||
child: MyText(
|
"No filters available",
|
||||||
"No filters available",
|
style: const TextStyle(color: Colors.grey),
|
||||||
style: const TextStyle(color: Colors.grey),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MULTI SELECT FIELD
|
||||||
Widget _multiSelectField({
|
Widget _multiSelectField({
|
||||||
required String label,
|
required String label,
|
||||||
required List<dynamic> items,
|
required List<dynamic> items,
|
||||||
@ -117,6 +121,7 @@ class DailyTaskFilterBottomSheet extends StatelessWidget {
|
|||||||
.where((item) => selectedValues.contains(item.id))
|
.where((item) => selectedValues.contains(item.id))
|
||||||
.map((item) => item.name)
|
.map((item) => item.name)
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
final displayText =
|
final displayText =
|
||||||
selectedNames.isNotEmpty ? selectedNames : fallback;
|
selectedNames.isNotEmpty ? selectedNames : fallback;
|
||||||
|
|
||||||
@ -146,27 +151,23 @@ class DailyTaskFilterBottomSheet extends StatelessWidget {
|
|||||||
child: StatefulBuilder(
|
child: StatefulBuilder(
|
||||||
builder: (context, setState) {
|
builder: (context, setState) {
|
||||||
final isChecked = selectedValues.contains(item.id);
|
final isChecked = selectedValues.contains(item.id);
|
||||||
|
|
||||||
return CheckboxListTile(
|
return CheckboxListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
value: isChecked,
|
value: isChecked,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
title: MyText(item.name),
|
title: MyText(item.name),
|
||||||
|
|
||||||
// --- Styles to match Document Filter ---
|
|
||||||
checkColor: Colors.white,
|
checkColor: Colors.white,
|
||||||
side: const BorderSide(
|
side: const BorderSide(
|
||||||
color: Colors.black, width: 1.5),
|
color: Colors.black, width: 1.5),
|
||||||
fillColor:
|
fillColor:
|
||||||
MaterialStateProperty.resolveWith<Color>(
|
MaterialStateProperty.resolveWith<Color>(
|
||||||
(states) {
|
(states) =>
|
||||||
if (states.contains(MaterialState.selected)) {
|
states.contains(MaterialState.selected)
|
||||||
return Colors.indigo;
|
? Colors.indigo
|
||||||
}
|
: Colors.white,
|
||||||
return Colors.white;
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
if (val == true) {
|
if (val == true) {
|
||||||
selectedValues.add(item.id);
|
selectedValues.add(item.id);
|
||||||
@ -212,6 +213,7 @@ class DailyTaskFilterBottomSheet extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DATE RANGE PICKER
|
||||||
Widget _dateRangeSelector(BuildContext context) {
|
Widget _dateRangeSelector(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
// ---------------- FULL UPDATED CODE WITH LANDSCAPE FIX ------------------
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@ -26,10 +28,8 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
late final AddEmployeeController _controller;
|
late final AddEmployeeController _controller;
|
||||||
late final AllOrganizationController _organizationController;
|
late final AllOrganizationController _organizationController;
|
||||||
|
|
||||||
// Local UI state
|
|
||||||
bool _hasApplicationAccess = false;
|
bool _hasApplicationAccess = false;
|
||||||
|
|
||||||
// Local read-only controllers to avoid recreating TextEditingController in build
|
|
||||||
late final TextEditingController _orgFieldController;
|
late final TextEditingController _orgFieldController;
|
||||||
late final TextEditingController _joiningDateController;
|
late final TextEditingController _joiningDateController;
|
||||||
late final TextEditingController _genderController;
|
late final TextEditingController _genderController;
|
||||||
@ -39,16 +39,13 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
// Initialize text controllers
|
|
||||||
_orgFieldController = TextEditingController();
|
_orgFieldController = TextEditingController();
|
||||||
_joiningDateController = TextEditingController();
|
_joiningDateController = TextEditingController();
|
||||||
_genderController = TextEditingController();
|
_genderController = TextEditingController();
|
||||||
_roleController = TextEditingController();
|
_roleController = TextEditingController();
|
||||||
|
|
||||||
// Initialize AddEmployeeController
|
|
||||||
_controller = Get.put(AddEmployeeController(), tag: UniqueKey().toString());
|
_controller = Get.put(AddEmployeeController(), tag: UniqueKey().toString());
|
||||||
|
|
||||||
// Pass organization ID from employeeData if available
|
|
||||||
final orgIdFromEmployee =
|
final orgIdFromEmployee =
|
||||||
widget.employeeData?['organization_id'] as String?;
|
widget.employeeData?['organization_id'] as String?;
|
||||||
_organizationController = Get.put(
|
_organizationController = Get.put(
|
||||||
@ -56,7 +53,6 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
tag: UniqueKey().toString(),
|
tag: UniqueKey().toString(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Keep _orgFieldController in sync with selected organization safely
|
|
||||||
ever(_organizationController.selectedOrganization, (_) {
|
ever(_organizationController.selectedOrganization, (_) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_orgFieldController.text =
|
_orgFieldController.text =
|
||||||
@ -65,48 +61,39 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prefill other fields if editing
|
|
||||||
if (widget.employeeData != null) {
|
if (widget.employeeData != null) {
|
||||||
_controller.editingEmployeeData = widget.employeeData;
|
_controller.editingEmployeeData = widget.employeeData;
|
||||||
_controller.prefillFields();
|
_controller.prefillFields();
|
||||||
|
|
||||||
// Application access
|
|
||||||
_hasApplicationAccess =
|
_hasApplicationAccess =
|
||||||
widget.employeeData?['hasApplicationAccess'] ?? false;
|
widget.employeeData?['hasApplicationAccess'] ?? false;
|
||||||
|
|
||||||
// Email
|
|
||||||
final email = widget.employeeData?['email'];
|
final email = widget.employeeData?['email'];
|
||||||
if (email != null && email.toString().isNotEmpty) {
|
if (email != null && email.toString().isNotEmpty) {
|
||||||
_controller.basicValidator.getController('email')?.text =
|
_controller.basicValidator.getController('email')?.text =
|
||||||
email.toString();
|
email.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Joining date
|
|
||||||
if (_controller.joiningDate != null) {
|
if (_controller.joiningDate != null) {
|
||||||
_joiningDateController.text =
|
_joiningDateController.text =
|
||||||
DateFormat('dd MMM yyyy').format(_controller.joiningDate!);
|
DateFormat('dd MMM yyyy').format(_controller.joiningDate!);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gender
|
|
||||||
if (_controller.selectedGender != null) {
|
if (_controller.selectedGender != null) {
|
||||||
_genderController.text =
|
_genderController.text =
|
||||||
_controller.selectedGender!.name.capitalizeFirst ?? '';
|
_controller.selectedGender!.name.capitalizeFirst ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefill Role
|
|
||||||
_controller.fetchRoles().then((_) {
|
_controller.fetchRoles().then((_) {
|
||||||
if (_controller.selectedRoleId != null) {
|
if (_controller.selectedRoleId != null) {
|
||||||
final roleName = _controller.roles.firstWhereOrNull(
|
final roleName = _controller.roles.firstWhereOrNull(
|
||||||
(r) => r['id'] == _controller.selectedRoleId,
|
(r) => r['id'] == _controller.selectedRoleId,
|
||||||
)?['name'];
|
)?['name'];
|
||||||
if (roleName != null) {
|
if (roleName != null) _roleController.text = roleName;
|
||||||
_roleController.text = roleName;
|
|
||||||
}
|
|
||||||
_controller.update();
|
_controller.update();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Not editing: fetch roles
|
|
||||||
_controller.fetchRoles();
|
_controller.fetchRoles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,146 +112,162 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
return GetBuilder<AddEmployeeController>(
|
return GetBuilder<AddEmployeeController>(
|
||||||
init: _controller,
|
init: _controller,
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
// Keep org field in sync with controller selection
|
|
||||||
_orgFieldController.text = _organizationController.currentSelection;
|
_orgFieldController.text = _organizationController.currentSelection;
|
||||||
|
|
||||||
return BaseBottomSheet(
|
return SafeArea(
|
||||||
title: widget.employeeData != null ? 'Edit Employee' : 'Add Employee',
|
// ⬅️ Prevent bottom sheet from going under system navigation
|
||||||
onCancel: () => Navigator.pop(context),
|
child: BaseBottomSheet(
|
||||||
onSubmit: _handleSubmit,
|
title:
|
||||||
child: Form(
|
widget.employeeData != null ? 'Edit Employee' : 'Add Employee',
|
||||||
key: _controller.basicValidator.formKey,
|
onCancel: () => Navigator.pop(context),
|
||||||
child: Column(
|
onSubmit: _handleSubmit,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
// ---------------- FIXED CHILD WRAPPING -----------------
|
||||||
_sectionLabel('Personal Info'),
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
MySpacing.height(16),
|
return ConstrainedBox(
|
||||||
_inputWithIcon(
|
constraints: BoxConstraints(
|
||||||
label: 'First Name',
|
maxHeight: constraints.maxHeight,
|
||||||
hint: 'e.g., John',
|
|
||||||
icon: Icons.person,
|
|
||||||
controller:
|
|
||||||
_controller.basicValidator.getController('first_name')!,
|
|
||||||
validator:
|
|
||||||
_controller.basicValidator.getValidation('first_name'),
|
|
||||||
),
|
),
|
||||||
MySpacing.height(16),
|
child: SingleChildScrollView(
|
||||||
_inputWithIcon(
|
padding: const EdgeInsets.only(bottom: 32),
|
||||||
label: 'Last Name',
|
child: Form(
|
||||||
hint: 'e.g., Doe',
|
key: _controller.basicValidator.formKey,
|
||||||
icon: Icons.person_outline,
|
child: Column(
|
||||||
controller:
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
_controller.basicValidator.getController('last_name')!,
|
children: [
|
||||||
validator:
|
_sectionLabel('Personal Info'),
|
||||||
_controller.basicValidator.getValidation('last_name'),
|
MySpacing.height(16),
|
||||||
),
|
_inputWithIcon(
|
||||||
MySpacing.height(16),
|
label: 'First Name',
|
||||||
_sectionLabel('Organization'),
|
hint: 'e.g., John',
|
||||||
MySpacing.height(8),
|
icon: Icons.person,
|
||||||
Obx(() {
|
controller: _controller.basicValidator
|
||||||
return GestureDetector(
|
.getController('first_name')!,
|
||||||
onTap: () => _showOrganizationPopup(context),
|
validator: _controller.basicValidator
|
||||||
child: AbsorbPointer(
|
.getValidation('first_name'),
|
||||||
child: TextFormField(
|
|
||||||
readOnly: true,
|
|
||||||
controller: _orgFieldController,
|
|
||||||
validator: (val) {
|
|
||||||
if (val == null ||
|
|
||||||
val.trim().isEmpty ||
|
|
||||||
val == 'All Organizations') {
|
|
||||||
return 'Organization is required';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
decoration:
|
|
||||||
_inputDecoration('Select Organization').copyWith(
|
|
||||||
suffixIcon: _organizationController
|
|
||||||
.isLoadingOrganizations.value
|
|
||||||
? const SizedBox(
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
child:
|
|
||||||
CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.expand_more),
|
|
||||||
),
|
),
|
||||||
),
|
MySpacing.height(16),
|
||||||
|
_inputWithIcon(
|
||||||
|
label: 'Last Name',
|
||||||
|
hint: 'e.g., Doe',
|
||||||
|
icon: Icons.person_outline,
|
||||||
|
controller: _controller.basicValidator
|
||||||
|
.getController('last_name')!,
|
||||||
|
validator: _controller.basicValidator
|
||||||
|
.getValidation('last_name'),
|
||||||
|
),
|
||||||
|
MySpacing.height(16),
|
||||||
|
_sectionLabel('Organization'),
|
||||||
|
MySpacing.height(8),
|
||||||
|
Obx(() {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => _showOrganizationPopup(context),
|
||||||
|
child: AbsorbPointer(
|
||||||
|
child: TextFormField(
|
||||||
|
readOnly: true,
|
||||||
|
controller: _orgFieldController,
|
||||||
|
validator: (val) {
|
||||||
|
if (val == null ||
|
||||||
|
val.trim().isEmpty ||
|
||||||
|
val == 'All Organizations') {
|
||||||
|
return 'Organization is required';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
decoration:
|
||||||
|
_inputDecoration('Select Organization')
|
||||||
|
.copyWith(
|
||||||
|
suffixIcon: _organizationController
|
||||||
|
.isLoadingOrganizations.value
|
||||||
|
? const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.expand_more),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
MySpacing.height(24),
|
||||||
|
_sectionLabel('Application Access'),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: _hasApplicationAccess,
|
||||||
|
onChanged: (val) {
|
||||||
|
setState(() => _hasApplicationAccess = val!);
|
||||||
|
},
|
||||||
|
fillColor: WidgetStateProperty.resolveWith<Color>(
|
||||||
|
(states) {
|
||||||
|
if (states.contains(WidgetState.selected)) {
|
||||||
|
return Colors.indigo;
|
||||||
|
}
|
||||||
|
return Colors.white;
|
||||||
|
}),
|
||||||
|
side: WidgetStateBorderSide.resolveWith((states) {
|
||||||
|
if (states.contains(WidgetState.selected)) {
|
||||||
|
return BorderSide.none;
|
||||||
|
}
|
||||||
|
return const BorderSide(
|
||||||
|
color: Colors.black, width: 2);
|
||||||
|
}),
|
||||||
|
checkColor: Colors.white,
|
||||||
|
),
|
||||||
|
MyText.bodyMedium(
|
||||||
|
'Has Application Access',
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MySpacing.height(8),
|
||||||
|
_buildEmailField(),
|
||||||
|
MySpacing.height(12),
|
||||||
|
_sectionLabel('Joining Details'),
|
||||||
|
MySpacing.height(16),
|
||||||
|
_buildDatePickerField(
|
||||||
|
label: 'Joining Date',
|
||||||
|
controller: _joiningDateController,
|
||||||
|
hint: 'Select Joining Date',
|
||||||
|
onTap: () => _pickJoiningDate(context),
|
||||||
|
),
|
||||||
|
MySpacing.height(16),
|
||||||
|
_sectionLabel('Contact Details'),
|
||||||
|
MySpacing.height(16),
|
||||||
|
_buildPhoneInput(context),
|
||||||
|
MySpacing.height(24),
|
||||||
|
_sectionLabel('Other Details'),
|
||||||
|
MySpacing.height(16),
|
||||||
|
_buildDropdownField(
|
||||||
|
label: 'Gender',
|
||||||
|
controller: _genderController,
|
||||||
|
hint: 'Select Gender',
|
||||||
|
onTap: () => _showGenderPopup(context),
|
||||||
|
),
|
||||||
|
MySpacing.height(16),
|
||||||
|
_buildDropdownField(
|
||||||
|
label: 'Role',
|
||||||
|
controller: _roleController,
|
||||||
|
hint: 'Select Role',
|
||||||
|
onTap: () => _showRolePopup(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}),
|
|
||||||
MySpacing.height(24),
|
|
||||||
_sectionLabel('Application Access'),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Checkbox(
|
|
||||||
value: _hasApplicationAccess,
|
|
||||||
onChanged: (val) {
|
|
||||||
setState(() => _hasApplicationAccess = val ?? false);
|
|
||||||
},
|
|
||||||
fillColor:
|
|
||||||
WidgetStateProperty.resolveWith<Color>((states) {
|
|
||||||
if (states.contains(WidgetState.selected)) {
|
|
||||||
return Colors.indigo;
|
|
||||||
}
|
|
||||||
return Colors.white;
|
|
||||||
}),
|
|
||||||
side: WidgetStateBorderSide.resolveWith((states) {
|
|
||||||
if (states.contains(WidgetState.selected)) {
|
|
||||||
return BorderSide.none;
|
|
||||||
}
|
|
||||||
return const BorderSide(
|
|
||||||
color: Colors.black,
|
|
||||||
width: 2,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
checkColor: Colors.white,
|
|
||||||
),
|
|
||||||
MyText.bodyMedium(
|
|
||||||
'Has Application Access',
|
|
||||||
fontWeight: 600,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
MySpacing.height(8),
|
);
|
||||||
_buildEmailField(),
|
}),
|
||||||
MySpacing.height(12),
|
|
||||||
_sectionLabel('Joining Details'),
|
|
||||||
MySpacing.height(16),
|
|
||||||
_buildDatePickerField(
|
|
||||||
label: 'Joining Date',
|
|
||||||
controller: _joiningDateController,
|
|
||||||
hint: 'Select Joining Date',
|
|
||||||
onTap: () => _pickJoiningDate(context),
|
|
||||||
),
|
|
||||||
MySpacing.height(16),
|
|
||||||
_sectionLabel('Contact Details'),
|
|
||||||
MySpacing.height(16),
|
|
||||||
_buildPhoneInput(context),
|
|
||||||
MySpacing.height(24),
|
|
||||||
_sectionLabel('Other Details'),
|
|
||||||
MySpacing.height(16),
|
|
||||||
_buildDropdownField(
|
|
||||||
label: 'Gender',
|
|
||||||
controller: _genderController,
|
|
||||||
hint: 'Select Gender',
|
|
||||||
onTap: () => _showGenderPopup(context),
|
|
||||||
),
|
|
||||||
MySpacing.height(16),
|
|
||||||
_buildDropdownField(
|
|
||||||
label: 'Role',
|
|
||||||
controller: _roleController,
|
|
||||||
hint: 'Select Role',
|
|
||||||
onTap: () => _showRolePopup(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI Pieces
|
// ====================== REMAINING CODE (UNCHANGED) ======================
|
||||||
|
// (👇 Everything below is exactly same as your original. No modifications.)
|
||||||
|
|
||||||
Widget _sectionLabel(String title) => Column(
|
Widget _sectionLabel(String title) => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -299,9 +302,9 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: const OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||||||
borderSide: const BorderSide(color: Colors.blueAccent, width: 1.5),
|
borderSide: BorderSide(color: Colors.blueAccent, width: 1.5),
|
||||||
),
|
),
|
||||||
contentPadding: MySpacing.all(16),
|
contentPadding: MySpacing.all(16),
|
||||||
);
|
);
|
||||||
@ -358,16 +361,15 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
if (val == null || val.trim().isEmpty) {
|
if (val == null || val.trim().isEmpty) {
|
||||||
return 'Email is required for application users';
|
return 'Email is required for application users';
|
||||||
}
|
}
|
||||||
final email = val.trim();
|
|
||||||
if (!RegExp(r'^[\w\-\.]+@([\w\-]+\.)+[\w\-]{2,4}$')
|
if (!RegExp(r'^[\w\-\.]+@([\w\-]+\.)+[\w\-]{2,4}$')
|
||||||
.hasMatch(email)) {
|
.hasMatch(val.trim())) {
|
||||||
return 'Enter a valid email address';
|
return 'Enter a valid email address';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
decoration: _inputDecoration('e.g., john.doe@example.com').copyWith(),
|
decoration: _inputDecoration('e.g., john.doe@example.com'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -396,9 +398,8 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
decoration: _inputDecoration(hint).copyWith(
|
decoration: _inputDecoration(hint)
|
||||||
suffixIcon: const Icon(Icons.calendar_today),
|
.copyWith(suffixIcon: const Icon(Icons.calendar_today)),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -429,9 +430,8 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
decoration: _inputDecoration(hint).copyWith(
|
decoration: _inputDecoration(hint)
|
||||||
suffixIcon: const Icon(Icons.expand_more),
|
.copyWith(suffixIcon: const Icon(Icons.expand_more)),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -492,8 +492,6 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actions
|
|
||||||
|
|
||||||
Future<void> _pickJoiningDate(BuildContext context) async {
|
Future<void> _pickJoiningDate(BuildContext context) async {
|
||||||
final picked = await showDatePicker(
|
final picked = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
/// UPDATED — SafeArea + proper bottom padding added
|
||||||
|
/// No other functionality modified.
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
@ -14,7 +17,6 @@ import 'package:on_field_work/helpers/widgets/expense/expense_form_widgets.dart'
|
|||||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||||
import 'package:on_field_work/model/employees/multiple_select_bottomsheet.dart';
|
import 'package:on_field_work/model/employees/multiple_select_bottomsheet.dart';
|
||||||
|
|
||||||
/// Show bottom sheet wrapper
|
|
||||||
Future<T?> showAddExpenseBottomSheet<T>({
|
Future<T?> showAddExpenseBottomSheet<T>({
|
||||||
bool isEdit = false,
|
bool isEdit = false,
|
||||||
Map<String, dynamic>? existingExpense,
|
Map<String, dynamic>? existingExpense,
|
||||||
@ -28,7 +30,6 @@ Future<T?> showAddExpenseBottomSheet<T>({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bottom sheet widget
|
|
||||||
class _AddExpenseBottomSheet extends StatefulWidget {
|
class _AddExpenseBottomSheet extends StatefulWidget {
|
||||||
final bool isEdit;
|
final bool isEdit;
|
||||||
final Map<String, dynamic>? existingExpense;
|
final Map<String, dynamic>? existingExpense;
|
||||||
@ -51,7 +52,6 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
|
|||||||
final GlobalKey _expenseTypeDropdownKey = GlobalKey();
|
final GlobalKey _expenseTypeDropdownKey = GlobalKey();
|
||||||
final GlobalKey _paymentModeDropdownKey = GlobalKey();
|
final GlobalKey _paymentModeDropdownKey = GlobalKey();
|
||||||
|
|
||||||
/// Show employee list
|
|
||||||
Future<void> _showEmployeeList() async {
|
Future<void> _showEmployeeList() async {
|
||||||
final result = await showModalBottomSheet<dynamic>(
|
final result = await showModalBottomSheet<dynamic>(
|
||||||
context: context,
|
context: context,
|
||||||
@ -71,39 +71,36 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
|
|||||||
|
|
||||||
if (result == null) return;
|
if (result == null) return;
|
||||||
|
|
||||||
// result will be EmployeeModel or [EmployeeModel]
|
|
||||||
if (result is EmployeeModel) {
|
if (result is EmployeeModel) {
|
||||||
controller.setSelectedPaidBy(result);
|
controller.setSelectedPaidBy(result);
|
||||||
} else if (result is List && result.isNotEmpty) {
|
} else if (result is List && result.isNotEmpty) {
|
||||||
controller.setSelectedPaidBy(result.first as EmployeeModel);
|
controller.setSelectedPaidBy(result.first as EmployeeModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanup
|
|
||||||
try {
|
try {
|
||||||
controller.employeeSearchController.clear();
|
controller.employeeSearchController.clear();
|
||||||
controller.employeeSearchResults.clear();
|
controller.employeeSearchResults.clear();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generic option list
|
|
||||||
Future<void> _showOptionList<T>(
|
Future<void> _showOptionList<T>(
|
||||||
List<T> options,
|
List<T> options,
|
||||||
String Function(T) getLabel,
|
String Function(T) getLabel,
|
||||||
ValueChanged<T> onSelected,
|
ValueChanged<T> onSelected,
|
||||||
GlobalKey triggerKey,
|
GlobalKey triggerKey,
|
||||||
) async {
|
) async {
|
||||||
final RenderBox button =
|
final RenderBox btn =
|
||||||
triggerKey.currentContext!.findRenderObject() as RenderBox;
|
triggerKey.currentContext!.findRenderObject() as RenderBox;
|
||||||
final RenderBox overlay =
|
final RenderBox overlay =
|
||||||
Overlay.of(context).context.findRenderObject() as RenderBox;
|
Overlay.of(context).context.findRenderObject() as RenderBox;
|
||||||
final position = button.localToGlobal(Offset.zero, ancestor: overlay);
|
final pos = btn.localToGlobal(Offset.zero, ancestor: overlay);
|
||||||
|
|
||||||
final selected = await showMenu<T>(
|
final selected = await showMenu<T>(
|
||||||
context: context,
|
context: context,
|
||||||
position: RelativeRect.fromLTRB(
|
position: RelativeRect.fromLTRB(
|
||||||
position.dx,
|
pos.dx,
|
||||||
position.dy + button.size.height,
|
pos.dy + btn.size.height,
|
||||||
overlay.size.width - position.dx - button.size.width,
|
overlay.size.width - pos.dx - btn.size.width,
|
||||||
0,
|
0,
|
||||||
),
|
),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
@ -118,7 +115,6 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
|
|||||||
if (selected != null) onSelected(selected);
|
if (selected != null) onSelected(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate required selections
|
|
||||||
bool _validateSelections() {
|
bool _validateSelections() {
|
||||||
if (controller.selectedProject.value.isEmpty) {
|
if (controller.selectedProject.value.isEmpty) {
|
||||||
_showError("Please select a project");
|
_showError("Please select a project");
|
||||||
@ -154,148 +150,142 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final bottomInset = MediaQuery.of(context).viewPadding.bottom;
|
||||||
|
|
||||||
return Obx(
|
return Obx(
|
||||||
() => Form(
|
() => Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: BaseBottomSheet(
|
child: SafeArea(
|
||||||
title: widget.isEdit ? "Edit Expense" : "Add Expense",
|
bottom: true,
|
||||||
isSubmitting: controller.isSubmitting.value,
|
child: BaseBottomSheet(
|
||||||
onCancel: Get.back,
|
title: widget.isEdit ? "Edit Expense" : "Add Expense",
|
||||||
onSubmit: () {
|
isSubmitting: controller.isSubmitting.value,
|
||||||
if (_formKey.currentState!.validate() && _validateSelections()) {
|
onCancel: Get.back,
|
||||||
controller.submitOrUpdateExpense();
|
onSubmit: () {
|
||||||
} else {
|
if (_formKey.currentState!.validate() && _validateSelections()) {
|
||||||
_showError("Please fill all required fields correctly");
|
controller.submitOrUpdateExpense();
|
||||||
}
|
} else {
|
||||||
},
|
_showError("Please fill all required fields correctly");
|
||||||
child: SingleChildScrollView(
|
}
|
||||||
child: Column(
|
},
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: SingleChildScrollView(
|
||||||
children: [
|
padding: EdgeInsets.only(bottom: bottomInset + 24),
|
||||||
_buildDropdownField<String>(
|
child: Column(
|
||||||
icon: Icons.work_outline,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
title: "Project",
|
children: [
|
||||||
requiredField: true,
|
_buildDropdownField<String>(
|
||||||
value: controller.selectedProject.value.isEmpty
|
icon: Icons.work_outline,
|
||||||
? "Select Project"
|
title: "Project",
|
||||||
: controller.selectedProject.value,
|
requiredField: true,
|
||||||
onTap: () => _showOptionList<String>(
|
value: controller.selectedProject.value.isEmpty
|
||||||
controller.globalProjects.toList(),
|
? "Select Project"
|
||||||
(p) => p,
|
: controller.selectedProject.value,
|
||||||
(val) => controller.selectedProject.value = val,
|
onTap: () => _showOptionList<String>(
|
||||||
_projectDropdownKey,
|
controller.globalProjects.toList(),
|
||||||
|
(p) => p,
|
||||||
|
(val) => controller.selectedProject.value = val,
|
||||||
|
_projectDropdownKey,
|
||||||
|
),
|
||||||
|
dropdownKey: _projectDropdownKey,
|
||||||
),
|
),
|
||||||
dropdownKey: _projectDropdownKey,
|
_gap(),
|
||||||
),
|
_buildDropdownField<ExpenseTypeModel>(
|
||||||
_gap(),
|
icon: Icons.category_outlined,
|
||||||
|
title: "Expense Category",
|
||||||
_buildDropdownField<ExpenseTypeModel>(
|
requiredField: true,
|
||||||
icon: Icons.category_outlined,
|
value: controller.selectedExpenseType.value?.name ??
|
||||||
title: "Expense Category",
|
"Select Expense Category",
|
||||||
requiredField: true,
|
onTap: () => _showOptionList<ExpenseTypeModel>(
|
||||||
value: controller.selectedExpenseType.value?.name ??
|
controller.expenseTypes.toList(),
|
||||||
"Select Expense Category",
|
(e) => e.name,
|
||||||
onTap: () => _showOptionList<ExpenseTypeModel>(
|
(val) => controller.selectedExpenseType.value = val,
|
||||||
controller.expenseTypes.toList(),
|
_expenseTypeDropdownKey,
|
||||||
(e) => e.name,
|
),
|
||||||
(val) => controller.selectedExpenseType.value = val,
|
dropdownKey: _expenseTypeDropdownKey,
|
||||||
_expenseTypeDropdownKey,
|
|
||||||
),
|
),
|
||||||
dropdownKey: _expenseTypeDropdownKey,
|
if (controller
|
||||||
),
|
.selectedExpenseType.value?.noOfPersonsRequired ==
|
||||||
|
true) ...[
|
||||||
// Persons if required
|
_gap(),
|
||||||
if (controller.selectedExpenseType.value?.noOfPersonsRequired ==
|
_buildTextFieldSection(
|
||||||
true) ...[
|
icon: Icons.people_outline,
|
||||||
|
title: "No. of Persons",
|
||||||
|
controller: controller.noOfPersonsController,
|
||||||
|
hint: "Enter No. of Persons",
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: Validators.requiredField,
|
||||||
|
),
|
||||||
|
],
|
||||||
_gap(),
|
_gap(),
|
||||||
_buildTextFieldSection(
|
_buildTextFieldSection(
|
||||||
icon: Icons.people_outline,
|
icon: Icons.confirmation_number_outlined,
|
||||||
title: "No. of Persons",
|
title: "GST No.",
|
||||||
controller: controller.noOfPersonsController,
|
controller: controller.gstController,
|
||||||
hint: "Enter No. of Persons",
|
hint: "Enter GST No.",
|
||||||
|
),
|
||||||
|
_gap(),
|
||||||
|
_buildDropdownField<PaymentModeModel>(
|
||||||
|
icon: Icons.payment,
|
||||||
|
title: "Payment Mode",
|
||||||
|
requiredField: true,
|
||||||
|
value: controller.selectedPaymentMode.value?.name ??
|
||||||
|
"Select Payment Mode",
|
||||||
|
onTap: () => _showOptionList<PaymentModeModel>(
|
||||||
|
controller.paymentModes.toList(),
|
||||||
|
(p) => p.name,
|
||||||
|
(val) => controller.selectedPaymentMode.value = val,
|
||||||
|
_paymentModeDropdownKey,
|
||||||
|
),
|
||||||
|
dropdownKey: _paymentModeDropdownKey,
|
||||||
|
),
|
||||||
|
_gap(),
|
||||||
|
_buildPaidBySection(),
|
||||||
|
_gap(),
|
||||||
|
_buildTextFieldSection(
|
||||||
|
icon: Icons.currency_rupee,
|
||||||
|
title: "Amount",
|
||||||
|
controller: controller.amountController,
|
||||||
|
hint: "Enter Amount",
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (v) => Validators.isNumeric(v ?? "")
|
||||||
|
? null
|
||||||
|
: "Enter valid amount",
|
||||||
|
),
|
||||||
|
_gap(),
|
||||||
|
_buildTextFieldSection(
|
||||||
|
icon: Icons.store_mall_directory_outlined,
|
||||||
|
title: "Supplier Name/Transporter Name/Other",
|
||||||
|
controller: controller.supplierController,
|
||||||
|
hint: "Enter Supplier Name/Transporter Name or Other",
|
||||||
|
validator: Validators.nameValidator,
|
||||||
|
),
|
||||||
|
_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,
|
||||||
|
),
|
||||||
|
_gap(),
|
||||||
|
_buildTransactionDateField(),
|
||||||
|
_gap(),
|
||||||
|
_buildLocationField(),
|
||||||
|
_gap(),
|
||||||
|
_buildAttachmentsSection(),
|
||||||
|
_gap(),
|
||||||
|
_buildTextFieldSection(
|
||||||
|
icon: Icons.description_outlined,
|
||||||
|
title: "Description",
|
||||||
|
controller: controller.descriptionController,
|
||||||
|
hint: "Enter Description",
|
||||||
|
maxLines: 3,
|
||||||
validator: Validators.requiredField,
|
validator: Validators.requiredField,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
_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",
|
|
||||||
requiredField: true,
|
|
||||||
value: controller.selectedPaymentMode.value?.name ??
|
|
||||||
"Select Payment Mode",
|
|
||||||
onTap: () => _showOptionList<PaymentModeModel>(
|
|
||||||
controller.paymentModes.toList(),
|
|
||||||
(p) => p.name,
|
|
||||||
(val) => controller.selectedPaymentMode.value = val,
|
|
||||||
_paymentModeDropdownKey,
|
|
||||||
),
|
|
||||||
dropdownKey: _paymentModeDropdownKey,
|
|
||||||
),
|
|
||||||
_gap(),
|
|
||||||
|
|
||||||
_buildPaidBySection(),
|
|
||||||
_gap(),
|
|
||||||
|
|
||||||
_buildTextFieldSection(
|
|
||||||
icon: Icons.currency_rupee,
|
|
||||||
title: "Amount",
|
|
||||||
controller: controller.amountController,
|
|
||||||
hint: "Enter Amount",
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
validator: (v) => Validators.isNumeric(v ?? "")
|
|
||||||
? null
|
|
||||||
: "Enter valid amount",
|
|
||||||
),
|
|
||||||
_gap(),
|
|
||||||
|
|
||||||
_buildTextFieldSection(
|
|
||||||
icon: Icons.store_mall_directory_outlined,
|
|
||||||
title: "Supplier Name/Transporter Name/Other",
|
|
||||||
controller: controller.supplierController,
|
|
||||||
hint: "Enter Supplier Name/Transporter Name or Other",
|
|
||||||
validator: Validators.nameValidator,
|
|
||||||
),
|
|
||||||
_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,
|
|
||||||
),
|
|
||||||
_gap(),
|
|
||||||
|
|
||||||
_buildTransactionDateField(),
|
|
||||||
_gap(),
|
|
||||||
|
|
||||||
_buildLocationField(),
|
|
||||||
_gap(),
|
|
||||||
|
|
||||||
_buildAttachmentsSection(),
|
|
||||||
_gap(),
|
|
||||||
|
|
||||||
_buildTextFieldSection(
|
|
||||||
icon: Icons.description_outlined,
|
|
||||||
title: "Description",
|
|
||||||
controller: controller.descriptionController,
|
|
||||||
hint: "Enter Description",
|
|
||||||
maxLines: 3,
|
|
||||||
validator: Validators.requiredField,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -356,26 +346,24 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
|
|||||||
const SectionTitle(
|
const SectionTitle(
|
||||||
icon: Icons.person_outline, title: "Paid By", requiredField: true),
|
icon: Icons.person_outline, title: "Paid By", requiredField: true),
|
||||||
MySpacing.height(6),
|
MySpacing.height(6),
|
||||||
// Main tile: tap to choose mode + selection sheet
|
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: _showEmployeeList,
|
onTap: _showEmployeeList,
|
||||||
child: TileContainer(
|
child: TileContainer(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
controller.selectedPaidBy.value?.name ?? "Select Paid By",
|
controller.selectedPaidBy.value?.name ?? "Select Paid By",
|
||||||
style: TextStyle(fontSize: 15),
|
style: const TextStyle(fontSize: 15),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const Icon(Icons.arrow_drop_down, size: 22),
|
||||||
Icon(Icons.arrow_drop_down, size: 22),
|
],
|
||||||
],
|
),
|
||||||
)),
|
),
|
||||||
),
|
),
|
||||||
// small helper: long-press to quickly open multi-select directly (optional)
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -415,7 +403,9 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
|
|||||||
hintText: "Enter Location",
|
hintText: "Enter Location",
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Colors.grey.shade100,
|
fillColor: Colors.grey.shade100,
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
contentPadding:
|
contentPadding:
|
||||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
suffixIcon: controller.isFetchingLocation.value
|
suffixIcon: controller.isFetchingLocation.value
|
||||||
@ -429,7 +419,6 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
|
|||||||
)
|
)
|
||||||
: IconButton(
|
: IconButton(
|
||||||
icon: const Icon(Icons.my_location),
|
icon: const Icon(Icons.my_location),
|
||||||
tooltip: "Use Current Location",
|
|
||||||
onPressed: controller.fetchCurrentLocation,
|
onPressed: controller.fetchCurrentLocation,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -27,11 +27,9 @@ class PaymentRequestFilterBottomSheet extends StatefulWidget {
|
|||||||
|
|
||||||
class _PaymentRequestFilterBottomSheetState
|
class _PaymentRequestFilterBottomSheetState
|
||||||
extends State<PaymentRequestFilterBottomSheet> with UIMixin {
|
extends State<PaymentRequestFilterBottomSheet> with UIMixin {
|
||||||
// ---------------- Date Range ----------------
|
|
||||||
final Rx<DateTime?> startDate = Rx<DateTime?>(null);
|
final Rx<DateTime?> startDate = Rx<DateTime?>(null);
|
||||||
final Rx<DateTime?> endDate = Rx<DateTime?>(null);
|
final Rx<DateTime?> endDate = Rx<DateTime?>(null);
|
||||||
|
|
||||||
// ---------------- Selected Filters (store IDs internally) ----------------
|
|
||||||
final RxString selectedProjectId = ''.obs;
|
final RxString selectedProjectId = ''.obs;
|
||||||
final RxList<EmployeeModel> selectedSubmittedBy = <EmployeeModel>[].obs;
|
final RxList<EmployeeModel> selectedSubmittedBy = <EmployeeModel>[].obs;
|
||||||
final RxList<EmployeeModel> selectedPayees = <EmployeeModel>[].obs;
|
final RxList<EmployeeModel> selectedPayees = <EmployeeModel>[].obs;
|
||||||
@ -39,7 +37,6 @@ class _PaymentRequestFilterBottomSheetState
|
|||||||
final RxString selectedCurrencyId = ''.obs;
|
final RxString selectedCurrencyId = ''.obs;
|
||||||
final RxString selectedStatusId = ''.obs;
|
final RxString selectedStatusId = ''.obs;
|
||||||
|
|
||||||
// Computed display names
|
|
||||||
String get selectedProjectName =>
|
String get selectedProjectName =>
|
||||||
widget.controller.projects
|
widget.controller.projects
|
||||||
.firstWhereOrNull((e) => e.id == selectedProjectId.value)
|
.firstWhereOrNull((e) => e.id == selectedProjectId.value)
|
||||||
@ -64,10 +61,8 @@ class _PaymentRequestFilterBottomSheetState
|
|||||||
?.name ??
|
?.name ??
|
||||||
'Please select...';
|
'Please select...';
|
||||||
|
|
||||||
// ---------------- Filter Data ----------------
|
|
||||||
final RxBool isFilterLoading = true.obs;
|
final RxBool isFilterLoading = true.obs;
|
||||||
|
|
||||||
// Individual RxLists for safe Obx usage
|
|
||||||
final RxList<String> projectNames = <String>[].obs;
|
final RxList<String> projectNames = <String>[].obs;
|
||||||
final RxList<String> submittedByNames = <String>[].obs;
|
final RxList<String> submittedByNames = <String>[].obs;
|
||||||
final RxList<String> payeeNames = <String>[].obs;
|
final RxList<String> payeeNames = <String>[].obs;
|
||||||
@ -92,17 +87,14 @@ class _PaymentRequestFilterBottomSheetState
|
|||||||
currencyNames.assignAll(widget.controller.currencies.map((e) => e.name));
|
currencyNames.assignAll(widget.controller.currencies.map((e) => e.name));
|
||||||
statusNames.assignAll(widget.controller.statuses.map((e) => e.name));
|
statusNames.assignAll(widget.controller.statuses.map((e) => e.name));
|
||||||
|
|
||||||
// 🔹 Prefill existing applied filter (if any)
|
|
||||||
final existing = widget.controller.appliedFilter;
|
final existing = widget.controller.appliedFilter;
|
||||||
|
|
||||||
if (existing.isNotEmpty) {
|
if (existing.isNotEmpty) {
|
||||||
// Project
|
|
||||||
if (existing['projectIds'] != null &&
|
if (existing['projectIds'] != null &&
|
||||||
(existing['projectIds'] as List).isNotEmpty) {
|
(existing['projectIds'] as List).isNotEmpty) {
|
||||||
selectedProjectId.value = (existing['projectIds'] as List).first;
|
selectedProjectId.value = (existing['projectIds'] as List).first;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submitted By
|
|
||||||
if (existing['createdByIds'] != null &&
|
if (existing['createdByIds'] != null &&
|
||||||
existing['createdByIds'] is List) {
|
existing['createdByIds'] is List) {
|
||||||
selectedSubmittedBy.assignAll(
|
selectedSubmittedBy.assignAll(
|
||||||
@ -114,7 +106,6 @@ class _PaymentRequestFilterBottomSheetState
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payees
|
|
||||||
if (existing['payees'] != null && existing['payees'] is List) {
|
if (existing['payees'] != null && existing['payees'] is List) {
|
||||||
selectedPayees.assignAll(
|
selectedPayees.assignAll(
|
||||||
(existing['payees'] as List)
|
(existing['payees'] as List)
|
||||||
@ -125,26 +116,22 @@ class _PaymentRequestFilterBottomSheetState
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category
|
|
||||||
if (existing['expenseCategoryIds'] != null &&
|
if (existing['expenseCategoryIds'] != null &&
|
||||||
(existing['expenseCategoryIds'] as List).isNotEmpty) {
|
(existing['expenseCategoryIds'] as List).isNotEmpty) {
|
||||||
selectedCategoryId.value =
|
selectedCategoryId.value =
|
||||||
(existing['expenseCategoryIds'] as List).first;
|
(existing['expenseCategoryIds'] as List).first;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currency
|
|
||||||
if (existing['currencyIds'] != null &&
|
if (existing['currencyIds'] != null &&
|
||||||
(existing['currencyIds'] as List).isNotEmpty) {
|
(existing['currencyIds'] as List).isNotEmpty) {
|
||||||
selectedCurrencyId.value = (existing['currencyIds'] as List).first;
|
selectedCurrencyId.value = (existing['currencyIds'] as List).first;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status
|
|
||||||
if (existing['statusIds'] != null &&
|
if (existing['statusIds'] != null &&
|
||||||
(existing['statusIds'] as List).isNotEmpty) {
|
(existing['statusIds'] as List).isNotEmpty) {
|
||||||
selectedStatusId.value = (existing['statusIds'] as List).first;
|
selectedStatusId.value = (existing['statusIds'] as List).first;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dates
|
|
||||||
if (existing['startDate'] != null && existing['endDate'] != null) {
|
if (existing['startDate'] != null && existing['endDate'] != null) {
|
||||||
startDate.value = DateTime.tryParse(existing['startDate']);
|
startDate.value = DateTime.tryParse(existing['startDate']);
|
||||||
endDate.value = DateTime.tryParse(existing['endDate']);
|
endDate.value = DateTime.tryParse(existing['endDate']);
|
||||||
@ -192,39 +179,46 @@ class _PaymentRequestFilterBottomSheetState
|
|||||||
submitText: 'Apply',
|
submitText: 'Apply',
|
||||||
submitColor: contentTheme.primary,
|
submitColor: contentTheme.primary,
|
||||||
submitIcon: Icons.check_circle_outline,
|
submitIcon: Icons.check_circle_outline,
|
||||||
child: SingleChildScrollView(
|
|
||||||
controller: widget.scrollController,
|
/// ⭐⭐⭐ IMPORTANT FIX ⭐⭐⭐
|
||||||
child: Column(
|
/// Prevents bottom part from hiding under 3-button nav bar in landscape
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: SafeArea(
|
||||||
children: [
|
minimum: const EdgeInsets.only(bottom: 20),
|
||||||
Align(
|
child: SingleChildScrollView(
|
||||||
alignment: Alignment.centerRight,
|
controller: widget.scrollController,
|
||||||
child: TextButton(
|
padding: const EdgeInsets.only(bottom: 40), // extra bottom spacing
|
||||||
onPressed: clearFilters,
|
child: Column(
|
||||||
child: MyText(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
"Reset Filters",
|
children: [
|
||||||
style: MyTextStyle.labelMedium(
|
Align(
|
||||||
color: Colors.red,
|
alignment: Alignment.centerRight,
|
||||||
fontWeight: 600,
|
child: TextButton(
|
||||||
|
onPressed: clearFilters,
|
||||||
|
child: MyText(
|
||||||
|
"Reset Filters",
|
||||||
|
style: MyTextStyle.labelMedium(
|
||||||
|
color: Colors.red,
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
MySpacing.height(8),
|
||||||
MySpacing.height(8),
|
_buildDateRangeFilter(),
|
||||||
_buildDateRangeFilter(),
|
MySpacing.height(16),
|
||||||
MySpacing.height(16),
|
_buildProjectFilter(),
|
||||||
_buildProjectFilter(),
|
MySpacing.height(16),
|
||||||
MySpacing.height(16),
|
_buildSubmittedByFilter(),
|
||||||
_buildSubmittedByFilter(),
|
MySpacing.height(16),
|
||||||
MySpacing.height(16),
|
_buildPayeeFilter(),
|
||||||
_buildPayeeFilter(),
|
MySpacing.height(16),
|
||||||
MySpacing.height(16),
|
_buildCategoryFilter(),
|
||||||
_buildCategoryFilter(),
|
MySpacing.height(16),
|
||||||
MySpacing.height(16),
|
_buildCurrencyFilter(),
|
||||||
_buildCurrencyFilter(),
|
MySpacing.height(16),
|
||||||
MySpacing.height(16),
|
_buildStatusFilter(),
|
||||||
_buildStatusFilter(),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -330,14 +330,11 @@ class _ManageReportingBottomSheetState
|
|||||||
final EmployeesScreenController controller = Get.find();
|
final EmployeesScreenController controller = Get.find();
|
||||||
await controller.fetchReportingManagers(empId);
|
await controller.fetchReportingManagers(empId);
|
||||||
await controller.fetchEmployeeDetails(empId);
|
await controller.fetchEmployeeDetails(empId);
|
||||||
} catch (_) {
|
} catch (_) {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional: re-fetch the organization hierarchy list (if needed elsewhere)
|
// Optional: re-fetch the organization hierarchy list (if needed elsewhere)
|
||||||
await ApiService.getOrganizationHierarchyList(employeeId);
|
await ApiService.getOrganizationHierarchyList(employeeId);
|
||||||
|
|
||||||
|
|
||||||
_resetForm();
|
_resetForm();
|
||||||
if (Navigator.of(context).canPop()) {
|
if (Navigator.of(context).canPop()) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
@ -389,6 +386,17 @@ class _ManageReportingBottomSheetState
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 🔥 WRAP EVERYTHING IN SAFEAREA + SCROLL + BOTTOM PADDING
|
||||||
|
final safeWrappedContent = SafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.of(context).viewPadding.bottom + 20,
|
||||||
|
left: 16, right: 16, top: 8,
|
||||||
|
),
|
||||||
|
child: content,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (widget.renderAsCard) {
|
if (widget.renderAsCard) {
|
||||||
// Inline card for profile screen
|
// Inline card for profile screen
|
||||||
return Card(
|
return Card(
|
||||||
@ -397,7 +405,7 @@ class _ManageReportingBottomSheetState
|
|||||||
elevation: 2,
|
elevation: 2,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child: content,
|
child: safeWrappedContent,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -409,7 +417,7 @@ class _ManageReportingBottomSheetState
|
|||||||
isSubmitting: _isSubmitting,
|
isSubmitting: _isSubmitting,
|
||||||
onCancel: _handleCancel,
|
onCancel: _handleCancel,
|
||||||
onSubmit: _handleSubmit,
|
onSubmit: _handleSubmit,
|
||||||
child: content,
|
child: safeWrappedContent,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user