enhacement of UI for mobile screen responsiveness

This commit is contained in:
Manish 2025-11-25 12:17:45 +05:30
parent 41112a3eea
commit 5bed5bd2f4
8 changed files with 513 additions and 484 deletions

View File

@ -95,7 +95,7 @@ class _SearchAndFilterState extends State<SearchAndFilter> with UIMixin {
@override
Widget build(BuildContext context) {
return Padding(
padding: MySpacing.fromLTRB(12, 10, 12, 0),
padding: MySpacing.fromLTRB(12, 10, 12, 8),
child: Row(
children: [
Expanded(
@ -179,13 +179,6 @@ class ToggleButtonsRow extends StatelessWidget {
decoration: BoxDecoration(
color: const Color(0xFFF0F0F0),
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
@ -286,8 +279,10 @@ class ExpenseList extends StatelessWidget {
return Center(child: MyText.bodyMedium('No expenses found.'));
}
return ListView.separated(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 80),
return SafeArea(
bottom: true,
child: ListView.separated(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 100),
itemCount: expenseList.length,
separatorBuilder: (_, __) =>
Divider(color: Colors.grey.shade300, height: 20),
@ -303,9 +298,7 @@ class ExpenseList extends StatelessWidget {
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () async {
await Get.to(
() => ExpenseDetailScreen(expenseId: expense.id),
);
await Get.to(() => ExpenseDetailScreen(expenseId: expense.id));
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
@ -321,7 +314,8 @@ class ExpenseList extends StatelessWidget {
children: [
MyText.bodyMedium('${expense.formattedAmount}',
fontWeight: 600),
if (expense.status.name.toLowerCase() == 'draft') ...[
if (expense.status.name.toLowerCase() ==
'draft') ...[
const SizedBox(width: 8),
GestureDetector(
onTap: () =>
@ -362,6 +356,7 @@ class ExpenseList extends StatelessWidget {
),
);
},
),
);
}
}

View File

@ -354,38 +354,27 @@ class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
final result = await showModalBottomSheet<List<EmployeeModel>>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
barrierColor: Colors.white,
useSafeArea: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) {
return DraggableScrollableSheet(
expand: false,
initialChildSize: 0.85,
minChildSize: 0.6,
maxChildSize: 1.0,
builder: (_, scrollController) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (_) => SizedBox(
height: MediaQuery.of(context).size.height * 0.90,
child: MultipleSelectRoleBottomSheet(
projectId: selectedProjectId!,
organizationId: selectedOrganization?.id,
serviceId: selectedService?.id,
roleId: selectedRoleId,
initiallySelected: controller.selectedEmployees.toList(),
scrollController: scrollController,
scrollController: ScrollController(),
),
),
);
},
);
},
);
if (result != null) {
controller.selectedEmployees
.assignAll(result); // RxList updates UI automatically
controller.selectedEmployees.assignAll(result);
}
}

View File

@ -38,10 +38,18 @@ class _DirectoryMainScreenState extends State<DirectoryMainScreen>
@override
Widget build(BuildContext context) {
return OrientationBuilder(
builder: (context, orientation) {
final bool isLandscape = orientation == Orientation.landscape;
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: PreferredSize(
preferredSize: const Size.fromHeight(72),
preferredSize: Size.fromHeight(
isLandscape ? 55 : 72, // Responsive height
),
child: SafeArea(
bottom: false,
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
@ -50,7 +58,6 @@ class _DirectoryMainScreenState extends State<DirectoryMainScreen>
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
@ -58,7 +65,9 @@ class _DirectoryMainScreenState extends State<DirectoryMainScreen>
onPressed: () => Get.offNamed('/dashboard'),
),
MySpacing.width(8),
Expanded(
/// FIX: Flexible to prevent overflow in landscape
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
@ -74,6 +83,7 @@ class _DirectoryMainScreenState extends State<DirectoryMainScreen>
final projectName =
projectController.selectedProject?.name ??
'Select Project';
return Row(
children: [
const Icon(Icons.work_outline,
@ -99,9 +109,13 @@ class _DirectoryMainScreenState extends State<DirectoryMainScreen>
),
),
),
body: Column(
),
/// MAIN CONTENT
body: SafeArea(
bottom: true,
child: Column(
children: [
// ---------------- TabBar ----------------
Container(
color: Colors.white,
child: TabBar(
@ -115,8 +129,6 @@ class _DirectoryMainScreenState extends State<DirectoryMainScreen>
],
),
),
// ---------------- TabBarView ----------------
Expanded(
child: TabBarView(
controller: _tabController,
@ -128,6 +140,9 @@ class _DirectoryMainScreenState extends State<DirectoryMainScreen>
),
],
),
),
);
},
);
}
}

View File

@ -49,12 +49,14 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(
0xFFF5F5F5),
backgroundColor: const Color(0xFFF5F5F5),
appBar: _buildAppBar(),
body: GestureDetector(
// SafeArea added so nothing hides under system navigation buttons
body: SafeArea(
bottom: true,
child: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: RefreshIndicator(
onRefresh: () async {
@ -69,9 +71,12 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
displacement: 60,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Container(
color:
const Color(0xFFF5F5F5),
child: Padding(
// Extra bottom padding so content does NOT go under 3-button navbar
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 20,
),
child: Column(
children: [
_buildSearchBar(),
@ -84,6 +89,7 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
),
),
),
),
);
}
@ -322,7 +328,6 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
);
}
// No employee selected yet
if (controller.selectedEmployee.value == null) {
return const Padding(
padding: EdgeInsets.only(top: 100),
@ -330,7 +335,6 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
);
}
// Employee selected but no payments found
if (controller.payments.isEmpty) {
return const Padding(
padding: EdgeInsets.only(top: 100),
@ -340,7 +344,6 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
);
}
// Payments available
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
@ -378,7 +381,7 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
decoration: BoxDecoration(
color: Colors.grey[100],
border: Border(
bottom: BorderSide(color: Color(0xFFE0E0E0), width: 0.9),
bottom: BorderSide(color: const Color(0xFFE0E0E0), width: 0.9),
),
),
child: Row(

View File

@ -113,14 +113,18 @@ class _FinanceScreenState extends State<FinanceScreen>
),
),
),
body: FadeTransition(
body: SafeArea(
top: false, // keep appbar area same
bottom: true, // avoid system bottom buttons
child: FadeTransition(
opacity: _fadeAnimation,
child: Obx(() {
if (menuController.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (menuController.hasError.value || menuController.menuItems.isEmpty) {
if (menuController.hasError.value ||
menuController.menuItems.isEmpty) {
return const Center(
child: Text(
"Failed to load menus. Please try again later.",
@ -129,7 +133,6 @@ class _FinanceScreenState extends State<FinanceScreen>
);
}
// Filter allowed Finance menus dynamically
final financeMenuIds = [
MenuItems.expenseReimbursement,
MenuItems.paymentRequests,
@ -149,8 +152,18 @@ class _FinanceScreenState extends State<FinanceScreen>
);
}
// ---- IMPORTANT FIX: Add bottom safe padding ----
final double bottomInset =
MediaQuery.of(context).viewPadding.bottom;
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
padding: EdgeInsets.fromLTRB(
16,
16,
16,
bottomInset +
24, // ensures charts never go under system buttons
),
child: Column(
children: [
_buildFinanceModulesCompact(financeMenus),
@ -165,16 +178,20 @@ class _FinanceScreenState extends State<FinanceScreen>
);
}),
),
),
);
}
// --- Finance Modules (Compact Dashboard-style) ---
Widget _buildFinanceModulesCompact(List<MenuItem> financeMenus) {
Widget _buildFinanceModulesCompact(List<MenuItem> financeMenus) {
// Map menu IDs to icon + color
final Map<String, _FinanceCardMeta> financeCardMeta = {
MenuItems.expenseReimbursement: _FinanceCardMeta(LucideIcons.badge_dollar_sign, contentTheme.info),
MenuItems.paymentRequests: _FinanceCardMeta(LucideIcons.receipt_text, contentTheme.primary),
MenuItems.advancePaymentStatements: _FinanceCardMeta(LucideIcons.wallet, contentTheme.warning),
MenuItems.expenseReimbursement:
_FinanceCardMeta(LucideIcons.badge_dollar_sign, contentTheme.info),
MenuItems.paymentRequests:
_FinanceCardMeta(LucideIcons.receipt_text, contentTheme.primary),
MenuItems.advancePaymentStatements:
_FinanceCardMeta(LucideIcons.wallet, contentTheme.warning),
};
// Build the stat items using API-provided mobileLink
@ -198,20 +215,22 @@ Widget _buildFinanceModulesCompact(List<MenuItem> financeMenus) {
return LayoutBuilder(builder: (context, constraints) {
// Determine number of columns dynamically
int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 4);
double cardWidth = (constraints.maxWidth - (crossAxisCount - 1) * 6) / crossAxisCount;
double cardWidth =
(constraints.maxWidth - (crossAxisCount - 1) * 6) / crossAxisCount;
return Wrap(
spacing: 6,
runSpacing: 6,
alignment: WrapAlignment.end,
children: stats
.map((stat) => _buildFinanceModuleCard(stat, projectSelected, cardWidth))
.map((stat) =>
_buildFinanceModuleCard(stat, projectSelected, cardWidth))
.toList(),
);
});
}
}
Widget _buildFinanceModuleCard(
Widget _buildFinanceModuleCard(
_FinanceStatItem stat, bool isProjectSelected, double width) {
return Opacity(
opacity: isProjectSelected ? 1.0 : 0.4, // Dim if no project selected
@ -260,9 +279,9 @@ Widget _buildFinanceModuleCard(
),
),
);
}
}
void _onCardTap(_FinanceStatItem statItem, bool isEnabled) {
void _onCardTap(_FinanceStatItem statItem, bool isEnabled) {
if (!isEnabled) {
Get.defaultDialog(
title: "No Project Selected",
@ -276,8 +295,8 @@ void _onCardTap(_FinanceStatItem statItem, bool isEnabled) {
// Navigate to the card's specific route
Get.toNamed(statItem.route);
}
}
}
}
class _FinanceStatItem {
final IconData icon;

View File

@ -99,7 +99,13 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
return Scaffold(
backgroundColor: Colors.white,
appBar: _buildAppBar(),
body: Column(
// ------------------------
// FIX: SafeArea prevents content from going under 3-button navbar
// ------------------------
body: SafeArea(
bottom: true,
child: Column(
children: [
Container(
color: Colors.white,
@ -135,6 +141,8 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
),
],
),
),
floatingActionButton: Obx(() {
if (permissionController.permissions.isEmpty) {
return const SizedBox.shrink();
@ -294,7 +302,6 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
final list = filteredList(isHistory: isHistory);
// ScrollController for infinite scroll
final scrollController = ScrollController();
scrollController.addListener(() {
if (scrollController.position.pixels >=
@ -309,6 +316,7 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
child: list.isEmpty
? ListView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.only(bottom: 100),
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.5,
@ -325,7 +333,12 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
)
: ListView.separated(
controller: scrollController,
padding: const EdgeInsets.fromLTRB(12, 12, 12, 80),
// ------------------------
// FIX: ensure bottom list items stay visible above nav bar
// ------------------------
padding: const EdgeInsets.fromLTRB(12, 12, 12, 120),
itemCount: list.length + 1,
separatorBuilder: (_, __) =>
Divider(color: Colors.grey.shade300, height: 20),
@ -365,10 +378,6 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
Row(
children: [
MyText.bodyMedium(item.expenseCategory.name, fontWeight: 600),
// -------------------------------
// ADV CHIP (only if advance)
// -------------------------------
if (item.isAdvancePayment == true) ...[
const SizedBox(width: 8),
Container(

View File

@ -51,6 +51,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
controller.fetchJobDetail(widget.jobId).then((_) {
final job = controller.jobDetail.value?.data;
if (job != null) {
_selectedTags.value = job.tags ?? [];
_titleController.text = job.title ?? '';
_descriptionController.text = job.description ?? '';
_startDateController.text = DateTimeUtils.convertUtcToLocal(
@ -169,6 +170,11 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
message: "Job updated successfully",
type: SnackbarType.success);
await controller.fetchJobDetail(widget.jobId);
final updatedJob = controller.jobDetail.value?.data;
if (updatedJob != null) {
_selectedTags.value = updatedJob.tags ?? [];
}
isEditing.value = false;
} else {
showAppSnackbar(

View File

@ -22,11 +22,11 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
final TextEditingController searchController = TextEditingController();
final ServiceProjectController controller =
Get.put(ServiceProjectController());
@override
void initState() {
super.initState();
// Fetch projects safely after first frame
WidgetsBinding.instance.addPostFrameCallback((_) {
controller.fetchProjects();
});
@ -49,7 +49,6 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
child: InkWell(
borderRadius: BorderRadius.circular(14),
onTap: () {
// Navigate to ServiceProjectDetailsScreen
Get.to(() => ServiceProjectDetailsScreen(
projectId: project.id,
projectName: project.name,
@ -60,7 +59,6 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Project Header
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -92,20 +90,14 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
),
],
),
MySpacing.height(10),
/// Assigned Date
_buildDetailRow(
Icons.date_range_outlined,
Colors.teal,
"Assigned: ${DateTimeUtils.convertUtcToLocal(project.assignedDate.toIso8601String(), format: DateTimeUtils.defaultFormat)}",
fontSize: 13,
),
MySpacing.height(8),
/// Client Info
if (project.client != null)
_buildDetailRow(
Icons.account_circle_outlined,
@ -113,20 +105,14 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
"Client: ${project.client!.name} (${project.client!.contactPerson})",
fontSize: 13,
),
MySpacing.height(8),
/// Contact Info
_buildDetailRow(
Icons.phone,
Colors.green,
"Contact: ${project.contactName} (${project.contactPhone})",
fontSize: 13,
),
MySpacing.height(12),
/// Services List
if (project.services.isNotEmpty)
Wrap(
spacing: 6,
@ -197,14 +183,18 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: CustomAppBar(
title: "Service Projects",
projectName: 'All Service Projects',
onBackPressed: () => Get.toNamed('/dashboard'),
),
body: Column(
// FIX 1: Entire body wrapped in SafeArea
body: SafeArea(
bottom: true,
child: Column(
children: [
/// Search bar and actions
Padding(
padding: MySpacing.xy(8, 8),
child: Row(
@ -253,8 +243,6 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
],
),
),
/// Project List
Expanded(
child: Obx(() {
if (controller.isLoading.value) {
@ -262,6 +250,7 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
}
final projects = controller.filteredProjects;
return MyRefreshIndicator(
onRefresh: _refreshProjects,
backgroundColor: Colors.indigo,
@ -270,8 +259,11 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
? _buildEmptyState()
: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
// FIX 2: Increased bottom padding for landscape
padding: MySpacing.only(
left: 8, right: 8, top: 4, bottom: 80),
left: 8, right: 8, top: 4, bottom: 120),
itemCount: projects.length,
separatorBuilder: (_, __) => MySpacing.height(12),
itemBuilder: (_, index) =>
@ -282,6 +274,7 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
),
],
),
),
);
}
}