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,82 +279,84 @@ class ExpenseList extends StatelessWidget {
return Center(child: MyText.bodyMedium('No expenses found.'));
}
return ListView.separated(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 80),
itemCount: expenseList.length,
separatorBuilder: (_, __) =>
Divider(color: Colors.grey.shade300, height: 20),
itemBuilder: (context, index) {
final expense = expenseList[index];
final formattedDate = DateTimeUtils.convertUtcToLocal(
expense.transactionDate.toIso8601String(),
format: 'dd MMM yyyy',
);
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),
itemBuilder: (context, index) {
final expense = expenseList[index];
final formattedDate = DateTimeUtils.convertUtcToLocal(
expense.transactionDate.toIso8601String(),
format: 'dd MMM yyyy',
);
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () async {
await Get.to(
() => ExpenseDetailScreen(expenseId: expense.id),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.bodyMedium(expense.expenseCategory.name,
fontWeight: 600),
Row(
children: [
MyText.bodyMedium('${expense.formattedAmount}',
fontWeight: 600),
if (expense.status.name.toLowerCase() == 'draft') ...[
const SizedBox(width: 8),
GestureDetector(
onTap: () =>
_showDeleteConfirmation(context, expense),
child: const Icon(Icons.delete,
color: Colors.red, size: 20),
),
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () async {
await Get.to(() => ExpenseDetailScreen(expenseId: expense.id));
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.bodyMedium(expense.expenseCategory.name,
fontWeight: 600),
Row(
children: [
MyText.bodyMedium('${expense.formattedAmount}',
fontWeight: 600),
if (expense.status.name.toLowerCase() ==
'draft') ...[
const SizedBox(width: 8),
GestureDetector(
onTap: () =>
_showDeleteConfirmation(context, expense),
child: const Icon(Icons.delete,
color: Colors.red, size: 20),
),
],
],
],
),
],
),
const SizedBox(height: 6),
Row(
children: [
MyText.bodySmall(formattedDate, fontWeight: 500),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Color(int.parse(
'0xff${expense.status.color.substring(1)}'))
.withOpacity(0.5),
borderRadius: BorderRadius.circular(5),
),
child: MyText.bodySmall(
expense.status.name,
color: Colors.white,
fontWeight: 500,
],
),
const SizedBox(height: 6),
Row(
children: [
MyText.bodySmall(formattedDate, fontWeight: 500),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Color(int.parse(
'0xff${expense.status.color.substring(1)}'))
.withOpacity(0.5),
borderRadius: BorderRadius.circular(5),
),
child: MyText.bodySmall(
expense.status.name,
color: Colors.white,
fontWeight: 500,
),
),
),
],
),
],
],
),
],
),
),
),
),
);
},
);
},
),
);
}
}

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)),
),
child: MultipleSelectRoleBottomSheet(
projectId: selectedProjectId!,
organizationId: selectedOrganization?.id,
serviceId: selectedService?.id,
roleId: selectedRoleId,
initiallySelected: controller.selectedEmployees.toList(),
scrollController: scrollController,
),
);
},
);
},
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(),
),
),
);
if (result != null) {
controller.selectedEmployees
.assignAll(result); // RxList updates UI automatically
controller.selectedEmployees.assignAll(result);
}
}

View File

@ -38,96 +38,111 @@ class _DirectoryMainScreenState extends State<DirectoryMainScreen>
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: PreferredSize(
preferredSize: const Size.fromHeight(72),
child: AppBar(
return OrientationBuilder(
builder: (context, orientation) {
final bool isLandscape = orientation == Orientation.landscape;
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard'),
),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
appBar: PreferredSize(
preferredSize: Size.fromHeight(
isLandscape ? 55 : 72, // Responsive height
),
child: SafeArea(
bottom: false,
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
children: [
MyText.titleLarge(
'Directory',
fontWeight: 700,
color: Colors.black,
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard'),
),
MySpacing.height(2),
GetBuilder<ProjectController>(
builder: (projectController) {
final projectName =
projectController.selectedProject?.name ??
'Select Project';
return Row(
children: [
const Icon(Icons.work_outline,
size: 14, color: Colors.grey),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
projectName,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
),
],
);
},
MySpacing.width(8),
/// FIX: Flexible to prevent overflow in landscape
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
MyText.titleLarge(
'Directory',
fontWeight: 700,
color: Colors.black,
),
MySpacing.height(2),
GetBuilder<ProjectController>(
builder: (projectController) {
final projectName =
projectController.selectedProject?.name ??
'Select Project';
return Row(
children: [
const Icon(Icons.work_outline,
size: 14, color: Colors.grey),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
projectName,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
),
],
);
},
),
],
),
),
],
),
),
),
),
),
/// MAIN CONTENT
body: SafeArea(
bottom: true,
child: Column(
children: [
Container(
color: Colors.white,
child: TabBar(
controller: _tabController,
labelColor: Colors.black,
unselectedLabelColor: Colors.grey,
indicatorColor: Colors.red,
tabs: const [
Tab(text: "Directory"),
Tab(text: "Notes"),
],
),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
DirectoryView(),
NotesView(),
],
),
),
],
),
),
),
),
body: Column(
children: [
// ---------------- TabBar ----------------
Container(
color: Colors.white,
child: TabBar(
controller: _tabController,
labelColor: Colors.black,
unselectedLabelColor: Colors.grey,
indicatorColor: Colors.red,
tabs: const [
Tab(text: "Directory"),
Tab(text: "Notes"),
],
),
),
// ---------------- TabBarView ----------------
Expanded(
child: TabBarView(
controller: _tabController,
children: [
DirectoryView(),
NotesView(),
],
),
),
],
),
);
},
);
}
}

View File

@ -49,36 +49,42 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(
0xFFF5F5F5),
backgroundColor: const Color(0xFFF5F5F5),
appBar: _buildAppBar(),
body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: RefreshIndicator(
onRefresh: () async {
final emp = controller.selectedEmployee.value;
if (emp != null) {
await controller.fetchAdvancePayments(emp.id.toString());
}
},
color: Colors.white,
backgroundColor: contentTheme.primary,
strokeWidth: 2.5,
displacement: 60,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Container(
color:
const Color(0xFFF5F5F5),
child: Column(
children: [
_buildSearchBar(),
_buildEmployeeDropdown(context),
_buildTopBalance(),
_buildPaymentList(),
],
// SafeArea added so nothing hides under system navigation buttons
body: SafeArea(
bottom: true,
child: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: RefreshIndicator(
onRefresh: () async {
final emp = controller.selectedEmployee.value;
if (emp != null) {
await controller.fetchAdvancePayments(emp.id.toString());
}
},
color: Colors.white,
backgroundColor: contentTheme.primary,
strokeWidth: 2.5,
displacement: 60,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
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(),
_buildEmployeeDropdown(context),
_buildTopBalance(),
_buildPaymentList(),
],
),
),
),
),
@ -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,171 +113,190 @@ class _FinanceScreenState extends State<FinanceScreen>
),
),
),
body: FadeTransition(
opacity: _fadeAnimation,
child: Obx(() {
if (menuController.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
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) {
return const Center(
child: Text(
"Failed to load menus. Please try again later.",
style: TextStyle(color: Colors.red),
if (menuController.hasError.value ||
menuController.menuItems.isEmpty) {
return const Center(
child: Text(
"Failed to load menus. Please try again later.",
style: TextStyle(color: Colors.red),
),
);
}
final financeMenuIds = [
MenuItems.expenseReimbursement,
MenuItems.paymentRequests,
MenuItems.advancePaymentStatements,
];
final financeMenus = menuController.menuItems
.where((m) => financeMenuIds.contains(m.id) && m.available)
.toList();
if (financeMenus.isEmpty) {
return const Center(
child: Text(
"You dont have access to the Finance section.",
style: TextStyle(color: Colors.grey),
),
);
}
// ---- IMPORTANT FIX: Add bottom safe padding ----
final double bottomInset =
MediaQuery.of(context).viewPadding.bottom;
return SingleChildScrollView(
padding: EdgeInsets.fromLTRB(
16,
16,
16,
bottomInset +
24, // ensures charts never go under system buttons
),
child: Column(
children: [
_buildFinanceModulesCompact(financeMenus),
MySpacing.height(24),
ExpenseByStatusWidget(controller: dashboardController),
MySpacing.height(24),
ExpenseTypeReportChart(),
MySpacing.height(24),
MonthlyExpenseDashboardChart(),
],
),
);
}
// Filter allowed Finance menus dynamically
final financeMenuIds = [
MenuItems.expenseReimbursement,
MenuItems.paymentRequests,
MenuItems.advancePaymentStatements,
];
final financeMenus = menuController.menuItems
.where((m) => financeMenuIds.contains(m.id) && m.available)
.toList();
if (financeMenus.isEmpty) {
return const Center(
child: Text(
"You dont have access to the Finance section.",
style: TextStyle(color: Colors.grey),
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildFinanceModulesCompact(financeMenus),
MySpacing.height(24),
ExpenseByStatusWidget(controller: dashboardController),
MySpacing.height(24),
ExpenseTypeReportChart(),
MySpacing.height(24),
MonthlyExpenseDashboardChart(),
],
),
);
}),
}),
),
),
);
}
// --- Finance Modules (Compact Dashboard-style) ---
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),
};
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),
};
// Build the stat items using API-provided mobileLink
final stats = financeMenus.map((menu) {
final meta = financeCardMeta[menu.id]!;
// Build the stat items using API-provided mobileLink
final stats = financeMenus.map((menu) {
final meta = financeCardMeta[menu.id]!;
// --- Log the routing info ---
debugPrint(
"[Finance Card] ID: ${menu.id}, Title: ${menu.name}, Route: ${menu.mobileLink}");
// --- Log the routing info ---
debugPrint(
"[Finance Card] ID: ${menu.id}, Title: ${menu.name}, Route: ${menu.mobileLink}");
return _FinanceStatItem(
meta.icon,
menu.name,
meta.color,
menu.mobileLink, // Each card navigates to its own route
);
}).toList();
return _FinanceStatItem(
meta.icon,
menu.name,
meta.color,
menu.mobileLink, // Each card navigates to its own route
);
}).toList();
final projectSelected = projectController.selectedProject != null;
final projectSelected = projectController.selectedProject != null;
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;
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;
return Wrap(
spacing: 6,
runSpacing: 6,
alignment: WrapAlignment.end,
children: stats
.map((stat) => _buildFinanceModuleCard(stat, projectSelected, cardWidth))
.toList(),
);
});
}
return Wrap(
spacing: 6,
runSpacing: 6,
alignment: WrapAlignment.end,
children: stats
.map((stat) =>
_buildFinanceModuleCard(stat, projectSelected, cardWidth))
.toList(),
);
});
}
Widget _buildFinanceModuleCard(
_FinanceStatItem stat, bool isProjectSelected, double width) {
return Opacity(
opacity: isProjectSelected ? 1.0 : 0.4, // Dim if no project selected
child: IgnorePointer(
ignoring: !isProjectSelected,
child: InkWell(
onTap: () => _onCardTap(stat, isProjectSelected),
borderRadius: BorderRadius.circular(5),
child: MyCard.bordered(
width: width,
height: 60,
paddingAll: 4,
borderRadiusAll: 5,
border: Border.all(color: Colors.grey.withOpacity(0.15)),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: stat.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Icon(
stat.icon,
size: 16,
color: stat.color,
),
),
MySpacing.height(4),
Flexible(
child: Text(
stat.title,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 10,
overflow: TextOverflow.ellipsis,
Widget _buildFinanceModuleCard(
_FinanceStatItem stat, bool isProjectSelected, double width) {
return Opacity(
opacity: isProjectSelected ? 1.0 : 0.4, // Dim if no project selected
child: IgnorePointer(
ignoring: !isProjectSelected,
child: InkWell(
onTap: () => _onCardTap(stat, isProjectSelected),
borderRadius: BorderRadius.circular(5),
child: MyCard.bordered(
width: width,
height: 60,
paddingAll: 4,
borderRadiusAll: 5,
border: Border.all(color: Colors.grey.withOpacity(0.15)),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: stat.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Icon(
stat.icon,
size: 16,
color: stat.color,
),
maxLines: 2,
softWrap: true,
),
),
],
MySpacing.height(4),
Flexible(
child: Text(
stat.title,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 10,
overflow: TextOverflow.ellipsis,
),
maxLines: 2,
softWrap: true,
),
),
],
),
),
),
),
),
);
}
void _onCardTap(_FinanceStatItem statItem, bool isEnabled) {
if (!isEnabled) {
Get.defaultDialog(
title: "No Project Selected",
middleText: "Please select a project before accessing this section.",
confirm: ElevatedButton(
onPressed: () => Get.back(),
child: const Text("OK"),
),
);
} else {
// Navigate to the card's specific route
Get.toNamed(statItem.route);
}
void _onCardTap(_FinanceStatItem statItem, bool isEnabled) {
if (!isEnabled) {
Get.defaultDialog(
title: "No Project Selected",
middleText: "Please select a project before accessing this section.",
confirm: ElevatedButton(
onPressed: () => Get.back(),
child: const Text("OK"),
),
);
} else {
// Navigate to the card's specific route
Get.toNamed(statItem.route);
}
}
}
}
class _FinanceStatItem {
final IconData icon;

View File

@ -99,42 +99,50 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
return Scaffold(
backgroundColor: Colors.white,
appBar: _buildAppBar(),
body: Column(
children: [
Container(
color: Colors.white,
child: TabBar(
controller: _tabController,
labelColor: Colors.black,
unselectedLabelColor: Colors.grey,
indicatorColor: Colors.red,
tabs: const [
Tab(text: "Current Month"),
Tab(text: "History"),
],
),
),
Expanded(
child: Container(
color: Colors.grey[100],
child: Column(
children: [
_buildSearchBar(),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildPaymentRequestList(isHistory: false),
_buildPaymentRequestList(isHistory: true),
],
),
),
// ------------------------
// FIX: SafeArea prevents content from going under 3-button navbar
// ------------------------
body: SafeArea(
bottom: true,
child: Column(
children: [
Container(
color: Colors.white,
child: TabBar(
controller: _tabController,
labelColor: Colors.black,
unselectedLabelColor: Colors.grey,
indicatorColor: Colors.red,
tabs: const [
Tab(text: "Current Month"),
Tab(text: "History"),
],
),
),
),
],
Expanded(
child: Container(
color: Colors.grey[100],
child: Column(
children: [
_buildSearchBar(),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildPaymentRequestList(isHistory: false),
_buildPaymentRequestList(isHistory: true),
],
),
),
],
),
),
),
],
),
),
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,90 +183,97 @@ 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(
children: [
/// Search bar and actions
Padding(
padding: MySpacing.xy(8, 8),
child: Row(
children: [
Expanded(
child: SizedBox(
height: 35,
child: TextField(
controller: searchController,
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(horizontal: 12),
prefixIcon: const Icon(Icons.search,
size: 20, color: Colors.grey),
suffixIcon: ValueListenableBuilder<TextEditingValue>(
valueListenable: searchController,
builder: (context, value, _) {
if (value.text.isEmpty) {
return const SizedBox.shrink();
}
return IconButton(
icon: const Icon(Icons.clear,
size: 20, color: Colors.grey),
onPressed: () {
searchController.clear();
controller.updateSearch('');
},
);
},
),
hintText: 'Search projects...',
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Colors.grey.shade300),
// FIX 1: Entire body wrapped in SafeArea
body: SafeArea(
bottom: true,
child: Column(
children: [
Padding(
padding: MySpacing.xy(8, 8),
child: Row(
children: [
Expanded(
child: SizedBox(
height: 35,
child: TextField(
controller: searchController,
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(horizontal: 12),
prefixIcon: const Icon(Icons.search,
size: 20, color: Colors.grey),
suffixIcon: ValueListenableBuilder<TextEditingValue>(
valueListenable: searchController,
builder: (context, value, _) {
if (value.text.isEmpty) {
return const SizedBox.shrink();
}
return IconButton(
icon: const Icon(Icons.clear,
size: 20, color: Colors.grey),
onPressed: () {
searchController.clear();
controller.updateSearch('');
},
);
},
),
hintText: 'Search projects...',
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Colors.grey.shade300),
),
),
),
),
),
),
],
],
),
),
),
Expanded(
child: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
/// Project List
Expanded(
child: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
final projects = controller.filteredProjects;
final projects = controller.filteredProjects;
return MyRefreshIndicator(
onRefresh: _refreshProjects,
backgroundColor: Colors.indigo,
color: Colors.white,
child: projects.isEmpty
? _buildEmptyState()
: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
padding: MySpacing.only(
left: 8, right: 8, top: 4, bottom: 80),
itemCount: projects.length,
separatorBuilder: (_, __) => MySpacing.height(12),
itemBuilder: (_, index) =>
_buildProjectCard(projects[index]),
),
);
}),
),
],
return MyRefreshIndicator(
onRefresh: _refreshProjects,
backgroundColor: Colors.indigo,
color: Colors.white,
child: projects.isEmpty
? _buildEmptyState()
: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
// FIX 2: Increased bottom padding for landscape
padding: MySpacing.only(
left: 8, right: 8, top: 4, bottom: 120),
itemCount: projects.length,
separatorBuilder: (_, __) => MySpacing.height(12),
itemBuilder: (_, index) =>
_buildProjectCard(projects[index]),
),
);
}),
),
],
),
),
);
}