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