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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: MySpacing.fromLTRB(12, 10, 12, 0), padding: MySpacing.fromLTRB(12, 10, 12, 8),
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
@ -179,13 +179,6 @@ class ToggleButtonsRow extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF0F0F0), color: const Color(0xFFF0F0F0),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
), ),
child: Row( child: Row(
children: [ children: [
@ -286,82 +279,84 @@ class ExpenseList extends StatelessWidget {
return Center(child: MyText.bodyMedium('No expenses found.')); return Center(child: MyText.bodyMedium('No expenses found.'));
} }
return ListView.separated( return SafeArea(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 80), bottom: true,
itemCount: expenseList.length, child: ListView.separated(
separatorBuilder: (_, __) => padding: const EdgeInsets.fromLTRB(12, 12, 12, 100),
Divider(color: Colors.grey.shade300, height: 20), itemCount: expenseList.length,
itemBuilder: (context, index) { separatorBuilder: (_, __) =>
final expense = expenseList[index]; Divider(color: Colors.grey.shade300, height: 20),
final formattedDate = DateTimeUtils.convertUtcToLocal( itemBuilder: (context, index) {
expense.transactionDate.toIso8601String(), final expense = expenseList[index];
format: 'dd MMM yyyy', final formattedDate = DateTimeUtils.convertUtcToLocal(
); expense.transactionDate.toIso8601String(),
format: 'dd MMM yyyy',
);
return Material( return Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
onTap: () async { onTap: () async {
await Get.to( await Get.to(() => ExpenseDetailScreen(expenseId: expense.id));
() => ExpenseDetailScreen(expenseId: expense.id), },
); child: Padding(
}, padding: const EdgeInsets.symmetric(vertical: 8),
child: Padding( child: Column(
padding: const EdgeInsets.symmetric(vertical: 8), crossAxisAlignment: CrossAxisAlignment.start,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceBetween,
Row( children: [
mainAxisAlignment: MainAxisAlignment.spaceBetween, MyText.bodyMedium(expense.expenseCategory.name,
children: [ fontWeight: 600),
MyText.bodyMedium(expense.expenseCategory.name, Row(
fontWeight: 600), children: [
Row( MyText.bodyMedium('${expense.formattedAmount}',
children: [ fontWeight: 600),
MyText.bodyMedium('${expense.formattedAmount}', if (expense.status.name.toLowerCase() ==
fontWeight: 600), 'draft') ...[
if (expense.status.name.toLowerCase() == 'draft') ...[ const SizedBox(width: 8),
const SizedBox(width: 8), GestureDetector(
GestureDetector( onTap: () =>
onTap: () => _showDeleteConfirmation(context, expense),
_showDeleteConfirmation(context, expense), child: const Icon(Icons.delete,
child: const Icon(Icons.delete, color: Colors.red, size: 20),
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, const SizedBox(height: 6),
fontWeight: 500, 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>>( final result = await showModalBottomSheet<List<EmployeeModel>>(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
backgroundColor: Colors.white,
barrierColor: Colors.white,
useSafeArea: true,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)), borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
), ),
builder: (context) { builder: (_) => SizedBox(
return DraggableScrollableSheet( height: MediaQuery.of(context).size.height * 0.90,
expand: false, child: MultipleSelectRoleBottomSheet(
initialChildSize: 0.85, projectId: selectedProjectId!,
minChildSize: 0.6, organizationId: selectedOrganization?.id,
maxChildSize: 1.0, serviceId: selectedService?.id,
builder: (_, scrollController) { roleId: selectedRoleId,
return Container( initiallySelected: controller.selectedEmployees.toList(),
decoration: const BoxDecoration( scrollController: ScrollController(),
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,
),
);
},
);
},
); );
if (result != null) { if (result != null) {
controller.selectedEmployees controller.selectedEmployees.assignAll(result);
.assignAll(result); // RxList updates UI automatically
} }
} }

View File

@ -38,96 +38,111 @@ class _DirectoryMainScreenState extends State<DirectoryMainScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return OrientationBuilder(
backgroundColor: const Color(0xFFF5F5F5), builder: (context, orientation) {
appBar: PreferredSize( final bool isLandscape = orientation == Orientation.landscape;
preferredSize: const Size.fromHeight(72),
child: AppBar( return Scaffold(
backgroundColor: const Color(0xFFF5F5F5), backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5, appBar: PreferredSize(
automaticallyImplyLeading: false, preferredSize: Size.fromHeight(
titleSpacing: 0, isLandscape ? 55 : 72, // Responsive height
title: Padding( ),
padding: MySpacing.xy(16, 0), child: SafeArea(
child: Row( bottom: false,
crossAxisAlignment: CrossAxisAlignment.center, child: AppBar(
children: [ backgroundColor: const Color(0xFFF5F5F5),
IconButton( elevation: 0.5,
icon: const Icon(Icons.arrow_back_ios_new, automaticallyImplyLeading: false,
color: Colors.black, size: 20), titleSpacing: 0,
onPressed: () => Get.offNamed('/dashboard'), title: Padding(
), padding: MySpacing.xy(16, 0),
MySpacing.width(8), child: Row(
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [ children: [
MyText.titleLarge( IconButton(
'Directory', icon: const Icon(Icons.arrow_back_ios_new,
fontWeight: 700, color: Colors.black, size: 20),
color: Colors.black, onPressed: () => Get.offNamed('/dashboard'),
), ),
MySpacing.height(2), MySpacing.width(8),
GetBuilder<ProjectController>(
builder: (projectController) { /// FIX: Flexible to prevent overflow in landscape
final projectName = Flexible(
projectController.selectedProject?.name ?? child: Column(
'Select Project'; crossAxisAlignment: CrossAxisAlignment.start,
return Row( mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.work_outline, MyText.titleLarge(
size: 14, color: Colors.grey), 'Directory',
MySpacing.width(4), fontWeight: 700,
Expanded( color: Colors.black,
child: MyText.bodySmall( ),
projectName, MySpacing.height(2),
fontWeight: 600, GetBuilder<ProjectController>(
overflow: TextOverflow.ellipsis, builder: (projectController) {
color: Colors.grey[700], 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: const Color( backgroundColor: const Color(0xFFF5F5F5),
0xFFF5F5F5),
appBar: _buildAppBar(), appBar: _buildAppBar(),
body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(), // SafeArea added so nothing hides under system navigation buttons
child: RefreshIndicator( body: SafeArea(
onRefresh: () async { bottom: true,
final emp = controller.selectedEmployee.value; child: GestureDetector(
if (emp != null) { onTap: () => FocusScope.of(context).unfocus(),
await controller.fetchAdvancePayments(emp.id.toString()); child: RefreshIndicator(
} onRefresh: () async {
}, final emp = controller.selectedEmployee.value;
color: Colors.white, if (emp != null) {
backgroundColor: contentTheme.primary, await controller.fetchAdvancePayments(emp.id.toString());
strokeWidth: 2.5, }
displacement: 60, },
child: SingleChildScrollView( color: Colors.white,
physics: const AlwaysScrollableScrollPhysics(), backgroundColor: contentTheme.primary,
child: Container( strokeWidth: 2.5,
color: displacement: 60,
const Color(0xFFF5F5F5), child: SingleChildScrollView(
child: Column( physics: const AlwaysScrollableScrollPhysics(),
children: [ child: Padding(
_buildSearchBar(), // Extra bottom padding so content does NOT go under 3-button navbar
_buildEmployeeDropdown(context), padding: EdgeInsets.only(
_buildTopBalance(), bottom: MediaQuery.of(context).padding.bottom + 20,
_buildPaymentList(), ),
],
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) { if (controller.selectedEmployee.value == null) {
return const Padding( return const Padding(
padding: EdgeInsets.only(top: 100), padding: EdgeInsets.only(top: 100),
@ -330,7 +335,6 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
); );
} }
// Employee selected but no payments found
if (controller.payments.isEmpty) { if (controller.payments.isEmpty) {
return const Padding( return const Padding(
padding: EdgeInsets.only(top: 100), padding: EdgeInsets.only(top: 100),
@ -340,7 +344,6 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
); );
} }
// Payments available
return ListView.builder( return ListView.builder(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
@ -378,7 +381,7 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[100], color: Colors.grey[100],
border: Border( border: Border(
bottom: BorderSide(color: Color(0xFFE0E0E0), width: 0.9), bottom: BorderSide(color: const Color(0xFFE0E0E0), width: 0.9),
), ),
), ),
child: Row( child: Row(

View File

@ -113,171 +113,190 @@ class _FinanceScreenState extends State<FinanceScreen>
), ),
), ),
), ),
body: FadeTransition( body: SafeArea(
opacity: _fadeAnimation, top: false, // keep appbar area same
child: Obx(() { bottom: true, // avoid system bottom buttons
if (menuController.isLoading.value) { child: FadeTransition(
return const Center(child: CircularProgressIndicator()); opacity: _fadeAnimation,
} child: Obx(() {
if (menuController.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (menuController.hasError.value || menuController.menuItems.isEmpty) { if (menuController.hasError.value ||
return const Center( menuController.menuItems.isEmpty) {
child: Text( return const Center(
"Failed to load menus. Please try again later.", child: Text(
style: TextStyle(color: Colors.red), "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) --- // --- Finance Modules (Compact Dashboard-style) ---
Widget _buildFinanceModulesCompact(List<MenuItem> financeMenus) { Widget _buildFinanceModulesCompact(List<MenuItem> financeMenus) {
// Map menu IDs to icon + color // Map menu IDs to icon + color
final Map<String, _FinanceCardMeta> financeCardMeta = { final Map<String, _FinanceCardMeta> financeCardMeta = {
MenuItems.expenseReimbursement: _FinanceCardMeta(LucideIcons.badge_dollar_sign, contentTheme.info), MenuItems.expenseReimbursement:
MenuItems.paymentRequests: _FinanceCardMeta(LucideIcons.receipt_text, contentTheme.primary), _FinanceCardMeta(LucideIcons.badge_dollar_sign, contentTheme.info),
MenuItems.advancePaymentStatements: _FinanceCardMeta(LucideIcons.wallet, contentTheme.warning), MenuItems.paymentRequests:
}; _FinanceCardMeta(LucideIcons.receipt_text, contentTheme.primary),
MenuItems.advancePaymentStatements:
_FinanceCardMeta(LucideIcons.wallet, contentTheme.warning),
};
// Build the stat items using API-provided mobileLink // Build the stat items using API-provided mobileLink
final stats = financeMenus.map((menu) { final stats = financeMenus.map((menu) {
final meta = financeCardMeta[menu.id]!; final meta = financeCardMeta[menu.id]!;
// --- Log the routing info --- // --- Log the routing info ---
debugPrint( debugPrint(
"[Finance Card] ID: ${menu.id}, Title: ${menu.name}, Route: ${menu.mobileLink}"); "[Finance Card] ID: ${menu.id}, Title: ${menu.name}, Route: ${menu.mobileLink}");
return _FinanceStatItem( return _FinanceStatItem(
meta.icon, meta.icon,
menu.name, menu.name,
meta.color, meta.color,
menu.mobileLink, // Each card navigates to its own route menu.mobileLink, // Each card navigates to its own route
); );
}).toList(); }).toList();
final projectSelected = projectController.selectedProject != null; final projectSelected = projectController.selectedProject != null;
return LayoutBuilder(builder: (context, constraints) { return LayoutBuilder(builder: (context, constraints) {
// Determine number of columns dynamically // Determine number of columns dynamically
int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 4); 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( return Wrap(
spacing: 6, spacing: 6,
runSpacing: 6, runSpacing: 6,
alignment: WrapAlignment.end, alignment: WrapAlignment.end,
children: stats children: stats
.map((stat) => _buildFinanceModuleCard(stat, projectSelected, cardWidth)) .map((stat) =>
.toList(), _buildFinanceModuleCard(stat, projectSelected, cardWidth))
); .toList(),
}); );
} });
}
Widget _buildFinanceModuleCard( Widget _buildFinanceModuleCard(
_FinanceStatItem stat, bool isProjectSelected, double width) { _FinanceStatItem stat, bool isProjectSelected, double width) {
return Opacity( return Opacity(
opacity: isProjectSelected ? 1.0 : 0.4, // Dim if no project selected opacity: isProjectSelected ? 1.0 : 0.4, // Dim if no project selected
child: IgnorePointer( child: IgnorePointer(
ignoring: !isProjectSelected, ignoring: !isProjectSelected,
child: InkWell( child: InkWell(
onTap: () => _onCardTap(stat, isProjectSelected), onTap: () => _onCardTap(stat, isProjectSelected),
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
child: MyCard.bordered( child: MyCard.bordered(
width: width, width: width,
height: 60, height: 60,
paddingAll: 4, paddingAll: 4,
borderRadiusAll: 5, borderRadiusAll: 5,
border: Border.all(color: Colors.grey.withOpacity(0.15)), border: Border.all(color: Colors.grey.withOpacity(0.15)),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: stat.color.withOpacity(0.1), color: stat.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: Icon( child: Icon(
stat.icon, stat.icon,
size: 16, size: 16,
color: stat.color, color: stat.color,
),
),
MySpacing.height(4),
Flexible(
child: Text(
stat.title,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 10,
overflow: TextOverflow.ellipsis,
), ),
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 { class _FinanceStatItem {
final IconData icon; final IconData icon;

View File

@ -99,42 +99,50 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
appBar: _buildAppBar(), appBar: _buildAppBar(),
body: Column(
children: [ // ------------------------
Container( // FIX: SafeArea prevents content from going under 3-button navbar
color: Colors.white, // ------------------------
child: TabBar( body: SafeArea(
controller: _tabController, bottom: true,
labelColor: Colors.black, child: Column(
unselectedLabelColor: Colors.grey, children: [
indicatorColor: Colors.red, Container(
tabs: const [ color: Colors.white,
Tab(text: "Current Month"), child: TabBar(
Tab(text: "History"), controller: _tabController,
], labelColor: Colors.black,
), unselectedLabelColor: Colors.grey,
), indicatorColor: Colors.red,
Expanded( tabs: const [
child: Container( Tab(text: "Current Month"),
color: Colors.grey[100], Tab(text: "History"),
child: Column(
children: [
_buildSearchBar(),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildPaymentRequestList(isHistory: false),
_buildPaymentRequestList(isHistory: true),
],
),
),
], ],
), ),
), ),
), 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(() { floatingActionButton: Obx(() {
if (permissionController.permissions.isEmpty) { if (permissionController.permissions.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
@ -294,7 +302,6 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
final list = filteredList(isHistory: isHistory); final list = filteredList(isHistory: isHistory);
// ScrollController for infinite scroll
final scrollController = ScrollController(); final scrollController = ScrollController();
scrollController.addListener(() { scrollController.addListener(() {
if (scrollController.position.pixels >= if (scrollController.position.pixels >=
@ -309,6 +316,7 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
child: list.isEmpty child: list.isEmpty
? ListView( ? ListView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.only(bottom: 100),
children: [ children: [
SizedBox( SizedBox(
height: MediaQuery.of(context).size.height * 0.5, height: MediaQuery.of(context).size.height * 0.5,
@ -325,7 +333,12 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
) )
: ListView.separated( : ListView.separated(
controller: scrollController, 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, itemCount: list.length + 1,
separatorBuilder: (_, __) => separatorBuilder: (_, __) =>
Divider(color: Colors.grey.shade300, height: 20), Divider(color: Colors.grey.shade300, height: 20),
@ -365,10 +378,6 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
Row( Row(
children: [ children: [
MyText.bodyMedium(item.expenseCategory.name, fontWeight: 600), MyText.bodyMedium(item.expenseCategory.name, fontWeight: 600),
// -------------------------------
// ADV CHIP (only if advance)
// -------------------------------
if (item.isAdvancePayment == true) ...[ if (item.isAdvancePayment == true) ...[
const SizedBox(width: 8), const SizedBox(width: 8),
Container( Container(

View File

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

View File

@ -22,11 +22,11 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
final TextEditingController searchController = TextEditingController(); final TextEditingController searchController = TextEditingController();
final ServiceProjectController controller = final ServiceProjectController controller =
Get.put(ServiceProjectController()); Get.put(ServiceProjectController());
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Fetch projects safely after first frame
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
controller.fetchProjects(); controller.fetchProjects();
}); });
@ -49,10 +49,9 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
onTap: () { onTap: () {
// Navigate to ServiceProjectDetailsScreen
Get.to(() => ServiceProjectDetailsScreen( Get.to(() => ServiceProjectDetailsScreen(
projectId: project.id, projectId: project.id,
projectName: project.name, projectName: project.name,
)); ));
}, },
child: Padding( child: Padding(
@ -60,7 +59,6 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
/// Project Header
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -92,20 +90,14 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
), ),
], ],
), ),
MySpacing.height(10), MySpacing.height(10),
/// Assigned Date
_buildDetailRow( _buildDetailRow(
Icons.date_range_outlined, Icons.date_range_outlined,
Colors.teal, Colors.teal,
"Assigned: ${DateTimeUtils.convertUtcToLocal(project.assignedDate.toIso8601String(), format: DateTimeUtils.defaultFormat)}", "Assigned: ${DateTimeUtils.convertUtcToLocal(project.assignedDate.toIso8601String(), format: DateTimeUtils.defaultFormat)}",
fontSize: 13, fontSize: 13,
), ),
MySpacing.height(8), MySpacing.height(8),
/// Client Info
if (project.client != null) if (project.client != null)
_buildDetailRow( _buildDetailRow(
Icons.account_circle_outlined, Icons.account_circle_outlined,
@ -113,20 +105,14 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
"Client: ${project.client!.name} (${project.client!.contactPerson})", "Client: ${project.client!.name} (${project.client!.contactPerson})",
fontSize: 13, fontSize: 13,
), ),
MySpacing.height(8), MySpacing.height(8),
/// Contact Info
_buildDetailRow( _buildDetailRow(
Icons.phone, Icons.phone,
Colors.green, Colors.green,
"Contact: ${project.contactName} (${project.contactPhone})", "Contact: ${project.contactName} (${project.contactPhone})",
fontSize: 13, fontSize: 13,
), ),
MySpacing.height(12), MySpacing.height(12),
/// Services List
if (project.services.isNotEmpty) if (project.services.isNotEmpty)
Wrap( Wrap(
spacing: 6, spacing: 6,
@ -197,90 +183,97 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF5F5F5), backgroundColor: const Color(0xFFF5F5F5),
appBar: CustomAppBar( appBar: CustomAppBar(
title: "Service Projects", title: "Service Projects",
projectName: 'All Service Projects', projectName: 'All Service Projects',
onBackPressed: () => Get.toNamed('/dashboard'), onBackPressed: () => Get.toNamed('/dashboard'),
), ),
body: Column(
children: [ // FIX 1: Entire body wrapped in SafeArea
/// Search bar and actions body: SafeArea(
Padding( bottom: true,
padding: MySpacing.xy(8, 8), child: Column(
child: Row( children: [
children: [ Padding(
Expanded( padding: MySpacing.xy(8, 8),
child: SizedBox( child: Row(
height: 35, children: [
child: TextField( Expanded(
controller: searchController, child: SizedBox(
decoration: InputDecoration( height: 35,
contentPadding: child: TextField(
const EdgeInsets.symmetric(horizontal: 12), controller: searchController,
prefixIcon: const Icon(Icons.search, decoration: InputDecoration(
size: 20, color: Colors.grey), contentPadding:
suffixIcon: ValueListenableBuilder<TextEditingValue>( const EdgeInsets.symmetric(horizontal: 12),
valueListenable: searchController, prefixIcon: const Icon(Icons.search,
builder: (context, value, _) { size: 20, color: Colors.grey),
if (value.text.isEmpty) { suffixIcon: ValueListenableBuilder<TextEditingValue>(
return const SizedBox.shrink(); valueListenable: searchController,
} builder: (context, value, _) {
return IconButton( if (value.text.isEmpty) {
icon: const Icon(Icons.clear, return const SizedBox.shrink();
size: 20, color: Colors.grey), }
onPressed: () { return IconButton(
searchController.clear(); icon: const Icon(Icons.clear,
controller.updateSearch(''); size: 20, color: Colors.grey),
}, onPressed: () {
); searchController.clear();
}, controller.updateSearch('');
), },
hintText: 'Search projects...', );
filled: true, },
fillColor: Colors.white, ),
border: OutlineInputBorder( hintText: 'Search projects...',
borderRadius: BorderRadius.circular(5), filled: true,
borderSide: BorderSide(color: Colors.grey.shade300), fillColor: Colors.white,
), border: OutlineInputBorder(
enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(5),
borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: Colors.grey.shade300),
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 final projects = controller.filteredProjects;
Expanded(
child: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
final projects = controller.filteredProjects; return MyRefreshIndicator(
return MyRefreshIndicator( onRefresh: _refreshProjects,
onRefresh: _refreshProjects, backgroundColor: Colors.indigo,
backgroundColor: Colors.indigo, color: Colors.white,
color: Colors.white, child: projects.isEmpty
child: projects.isEmpty ? _buildEmptyState()
? _buildEmptyState() : ListView.separated(
: ListView.separated( physics: const AlwaysScrollableScrollPhysics(),
physics: const AlwaysScrollableScrollPhysics(),
padding: MySpacing.only( // FIX 2: Increased bottom padding for landscape
left: 8, right: 8, top: 4, bottom: 80), padding: MySpacing.only(
itemCount: projects.length, left: 8, right: 8, top: 4, bottom: 120),
separatorBuilder: (_, __) => MySpacing.height(12),
itemBuilder: (_, index) => itemCount: projects.length,
_buildProjectCard(projects[index]), separatorBuilder: (_, __) => MySpacing.height(12),
), itemBuilder: (_, index) =>
); _buildProjectCard(projects[index]),
}), ),
), );
], }),
),
],
),
), ),
); );
} }