diff --git a/lib/helpers/widgets/expense/expense_main_components.dart b/lib/helpers/widgets/expense/expense_main_components.dart index dccfff1..a2b36f1 100644 --- a/lib/helpers/widgets/expense/expense_main_components.dart +++ b/lib/helpers/widgets/expense/expense_main_components.dart @@ -95,7 +95,7 @@ class _SearchAndFilterState extends State 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, + ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), - ), - ); - }, + ); + }, + ), ); } } diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index 69b4104..ca909cd 100644 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -354,38 +354,27 @@ class _AssignTaskBottomSheetState extends State { final result = await showModalBottomSheet>( 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); } } diff --git a/lib/view/directory/directory_main_screen.dart b/lib/view/directory/directory_main_screen.dart index 473efaf..0295dcd 100644 --- a/lib/view/directory/directory_main_screen.dart +++ b/lib/view/directory/directory_main_screen.dart @@ -38,96 +38,111 @@ class _DirectoryMainScreenState extends State @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( - 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( + 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(), - ], - ), - ), - ], - ), + ); + }, ); } } diff --git a/lib/view/finance/advance_payment_screen.dart b/lib/view/finance/advance_payment_screen.dart index afa41bb..0334c45 100644 --- a/lib/view/finance/advance_payment_screen.dart +++ b/lib/view/finance/advance_payment_screen.dart @@ -49,36 +49,42 @@ class _AdvancePaymentScreenState extends State @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 ); } - // ✅ 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 ); } - // ✅ 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 ); } - // ✅ Payments available return ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -378,7 +381,7 @@ class _AdvancePaymentScreenState extends State 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( diff --git a/lib/view/finance/finance_screen.dart b/lib/view/finance/finance_screen.dart index 32cba67..55e7716 100644 --- a/lib/view/finance/finance_screen.dart +++ b/lib/view/finance/finance_screen.dart @@ -113,171 +113,190 @@ class _FinanceScreenState extends State ), ), ), - 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 don’t 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 don’t 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 financeMenus) { - // Map menu IDs to icon + color - final Map 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 financeMenus) { + // Map menu IDs to icon + color + final Map 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; diff --git a/lib/view/finance/payment_request_screen.dart b/lib/view/finance/payment_request_screen.dart index e259b97..5c46a37 100644 --- a/lib/view/finance/payment_request_screen.dart +++ b/lib/view/finance/payment_request_screen.dart @@ -99,42 +99,50 @@ class _PaymentRequestMainScreenState extends State 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 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 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 ) : 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 Row( children: [ MyText.bodyMedium(item.expenseCategory.name, fontWeight: 600), - - // ------------------------------- - // ADV CHIP (only if advance) - // ------------------------------- if (item.isAdvancePayment == true) ...[ const SizedBox(width: 8), Container( diff --git a/lib/view/service_project/service_project_job_detail_screen.dart b/lib/view/service_project/service_project_job_detail_screen.dart index 977c323..ad1c019 100644 --- a/lib/view/service_project/service_project_job_detail_screen.dart +++ b/lib/view/service_project/service_project_job_detail_screen.dart @@ -51,6 +51,7 @@ class _JobDetailsScreenState extends State 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 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( diff --git a/lib/view/service_project/service_project_screen.dart b/lib/view/service_project/service_project_screen.dart index bf07d58..7b28aff 100644 --- a/lib/view/service_project/service_project_screen.dart +++ b/lib/view/service_project/service_project_screen.dart @@ -22,11 +22,11 @@ class _ServiceProjectScreenState extends State 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,10 +49,9 @@ class _ServiceProjectScreenState extends State child: InkWell( borderRadius: BorderRadius.circular(14), onTap: () { - // Navigate to ServiceProjectDetailsScreen Get.to(() => ServiceProjectDetailsScreen( projectId: project.id, - projectName: project.name, + projectName: project.name, )); }, child: Padding( @@ -60,7 +59,6 @@ class _ServiceProjectScreenState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - /// Project Header Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -92,20 +90,14 @@ class _ServiceProjectScreenState extends State ), ], ), - 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 "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 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( - 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( + 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]), + ), + ); + }), + ), + ], + ), ), ); }