diff --git a/lib/helpers/widgets/expense/expense_main_components.dart b/lib/helpers/widgets/expense/expense_main_components.dart index dccfff1..e000f68 100644 --- a/lib/helpers/widgets/expense/expense_main_components.dart +++ b/lib/helpers/widgets/expense/expense_main_components.dart @@ -282,86 +282,129 @@ class ExpenseList extends StatelessWidget { @override Widget build(BuildContext context) { - if (expenseList.isEmpty && !Get.find().isLoading.value) { - return Center(child: MyText.bodyMedium('No expenses found.')); - } + final ExpenseController controller = Get.find(); - 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( + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = constraints.maxWidth > constraints.maxHeight; - 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, + if (controller.isLoading.value && expenseList.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + if (expenseList.isEmpty) { + return const Center( + child: Text( + 'No expenses found.', + style: TextStyle(color: Colors.grey), + ), + ); + } + + // PORTRAIT MODE + if (!isLandscape) { + return ListView.separated( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 80), + itemCount: expenseList.length, + separatorBuilder: (_, __) => + Divider(color: Colors.grey.shade300, height: 20), + itemBuilder: _buildItem, + ); + } + + // LANDSCAPE โ†’ WRAP IN SCROLL FOR SAFETY + return SingleChildScrollView( + child: SizedBox( + height: constraints.maxHeight * 1.3, + child: ListView.separated( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 80), + itemCount: expenseList.length, + separatorBuilder: (_, __) => + Divider(color: Colors.grey.shade300, height: 20), + itemBuilder: _buildItem, + ), + ), + ); + }, + ), + ); + } + + Widget _buildItem(BuildContext context, int 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 { + final result = await Get.to( + () => ExpenseDetailScreen(expenseId: expense.id), + arguments: {'expense': expense}, + ); + if (result == true && onViewDetail != null) { + await onViewDetail!(); + } + }, + 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( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - MyText.bodyMedium(expense.expenseCategory.name, + MyText.bodyMedium('${expense.formattedAmount}', 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), + 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), ), - 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/attendance/attendence_filter_sheet.dart b/lib/model/attendance/attendence_filter_sheet.dart index 38e1fc8..628e8dd 100644 --- a/lib/model/attendance/attendence_filter_sheet.dart +++ b/lib/model/attendance/attendence_filter_sheet.dart @@ -123,7 +123,6 @@ class _AttendanceFilterBottomSheetState }).toList(); final List widgets = [ - // ๐Ÿ”น View Section Padding( padding: const EdgeInsets.only(bottom: 4), child: Align( @@ -146,7 +145,6 @@ class _AttendanceFilterBottomSheetState }), ]; - // ๐Ÿ”น Organization filter widgets.addAll([ const Divider(), Padding( @@ -165,24 +163,6 @@ class _AttendanceFilterBottomSheetState color: Colors.grey.shade300, borderRadius: BorderRadius.circular(12), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - width: 100, - height: 14, - color: Colors.grey.shade400, - ), - Container( - width: 18, - height: 18, - decoration: BoxDecoration( - color: Colors.grey.shade400, - shape: BoxShape.circle, - ), - ), - ], - ), ); } else if (widget.controller.organizations.isEmpty) { return Center( @@ -200,7 +180,6 @@ class _AttendanceFilterBottomSheetState }), ]); - // ๐Ÿ”น Date Range (only for Attendance Logs) if (tempSelectedTab == 'attendanceLogs') { widgets.addAll([ const Divider(), @@ -211,16 +190,12 @@ class _AttendanceFilterBottomSheetState child: MyText.titleSmall("Date Range", fontWeight: 600), ), ), - // โœ… Reusable DateRangePickerWidget DateRangePickerWidget( startDate: widget.controller.startDateAttendance, endDate: widget.controller.endDateAttendance, startLabel: "Start Date", endLabel: "End Date", - onDateRangeSelected: (start, end) { - // Optional: trigger UI updates if needed - setState(() {}); - }, + onDateRangeSelected: (_, __) => setState(() {}), ), ]); } @@ -232,18 +207,36 @@ class _AttendanceFilterBottomSheetState Widget build(BuildContext context) { return ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), - child: BaseBottomSheet( - title: "Attendance Filter", - submitText: "Apply", - onCancel: () => Navigator.pop(context), - onSubmit: () => Navigator.pop(context, { - 'selectedTab': tempSelectedTab, - 'selectedOrganization': widget.controller.selectedOrganization?.id, - }), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: buildMainFilters(), - ), + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = constraints.maxWidth > constraints.maxHeight; + + return BaseBottomSheet( + title: "Attendance Filter", + submitText: "Apply", + onCancel: () => Navigator.pop(context), + onSubmit: () => Navigator.pop(context, { + 'selectedTab': tempSelectedTab, + 'selectedOrganization': + widget.controller.selectedOrganization?.id, + }), + + // ---------------- UPDATED RESPONSIVE CHILD ---------------- + child: SizedBox( + height: isLandscape + ? constraints.maxHeight // ๐Ÿ”ฅ Full screen in landscape + : constraints.maxHeight * 0.78, // normal in portrait + + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: buildMainFilters(), + ), + ), + ), + ); + }, ), ); } diff --git a/lib/model/finance/add_payment_request_bottom_sheet.dart b/lib/model/finance/add_payment_request_bottom_sheet.dart index b27dde4..630885d 100644 --- a/lib/model/finance/add_payment_request_bottom_sheet.dart +++ b/lib/model/finance/add_payment_request_bottom_sheet.dart @@ -58,12 +58,10 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> if (widget.isEdit && widget.existingData != null) { final data = widget.existingData!; - // Prefill text fields controller.titleController.text = data["title"] ?? ""; controller.amountController.text = data["amount"]?.toString() ?? ""; controller.descriptionController.text = data["description"] ?? ""; - // Prefill due date if (data["dueDate"] != null && data["dueDate"].toString().isNotEmpty) { DateTime? dueDate = DateTime.tryParse(data["dueDate"].toString()); if (dueDate != null) { @@ -73,15 +71,14 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> } } - // Prefill dropdowns & toggles controller.selectedProject.value = { 'id': data["projectId"], 'name': data["projectName"], }; + controller.selectedPayee.value = data["payee"] ?? ""; controller.isAdvancePayment.value = data["isAdvancePayment"] ?? false; - // Categories & currencies everAll([controller.categories, controller.currencies], (_) { controller.selectedCategory.value = controller.categories .firstWhereOrNull((c) => c.id == data["expenseCategoryId"]); @@ -89,7 +86,6 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> .firstWhereOrNull((c) => c.id == data["currencyId"]); }); - // Attachments final attachmentsData = data["attachments"]; if (attachmentsData != null && attachmentsData is List && @@ -116,51 +112,56 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> @override Widget build(BuildContext context) { - return Obx(() => Form( - key: _formKey, - child: BaseBottomSheet( - title: widget.isEdit - ? "Edit Payment Request" - : "Create Payment Request", - isSubmitting: controller.isSubmitting.value, - onCancel: Get.back, - submitText: "Save as Draft", - onSubmit: () async { - if (_formKey.currentState!.validate() && _validateSelections()) { - bool success = false; - if (widget.isEdit && widget.existingData != null) { - final requestId = - widget.existingData!['id']?.toString() ?? ''; - if (requestId.isNotEmpty) { - success = await controller.submitEditedPaymentRequest( - requestId: requestId); + return Obx(() => SafeArea( + child: Form( + key: _formKey, + child: BaseBottomSheet( + title: widget.isEdit + ? "Edit Payment Request" + : "Create Payment Request", + isSubmitting: controller.isSubmitting.value, + onCancel: Get.back, + submitText: "Save as Draft", + onSubmit: () async { + if (_formKey.currentState!.validate() && + _validateSelections()) { + bool success = false; + + if (widget.isEdit && widget.existingData != null) { + final requestId = + widget.existingData!['id']?.toString() ?? ''; + if (requestId.isNotEmpty) { + success = await controller.submitEditedPaymentRequest( + requestId: requestId); + } else { + _showError("Invalid Payment Request ID"); + return; + } } else { - _showError("Invalid Payment Request ID"); - return; + success = await controller.submitPaymentRequest(); } - } else { - success = await controller.submitPaymentRequest(); - } - if (success) { - Get.back(); - if (widget.onUpdated != null) widget.onUpdated!(); + if (success) { + Get.back(); + widget.onUpdated?.call(); - showAppSnackbar( - title: "Success", - message: widget.isEdit - ? "Payment request updated successfully!" - : "Payment request created successfully!", - type: SnackbarType.success, - ); + showAppSnackbar( + title: "Success", + message: widget.isEdit + ? "Payment request updated successfully!" + : "Payment request created successfully!", + type: SnackbarType.success, + ); + } } - } - }, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildDropdown( + }, + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildDropdown( "Select Project", Icons.work_outline, controller.selectedProject.value?['name'] ?? @@ -168,9 +169,10 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> controller.globalProjects, (p) => p['name'], controller.selectProject, - key: _projectDropdownKey), - _gap(), - _buildDropdown( + key: _projectDropdownKey, + ), + _gap(), + _buildDropdown( "Expense Category", Icons.category_outlined, controller.selectedCategory.value?.name ?? @@ -178,30 +180,35 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> controller.categories, (c) => c.name, controller.selectCategory, - key: _categoryDropdownKey), - _gap(), - _buildTextField( - "Title", Icons.title_outlined, controller.titleController, - hint: "Enter title", validator: Validators.requiredField), - _gap(), - _buildRadio("Is Advance Payment", Icons.attach_money_outlined, - controller.isAdvancePayment, ["Yes", "No"]), - _gap(), - _buildDueDateField(), - _gap(), - _buildTextField("Amount", Icons.currency_rupee, - controller.amountController, - hint: "Enter Amount", - keyboardType: TextInputType.number, - validator: (v) => (v != null && - v.isNotEmpty && - double.tryParse(v) != null) - ? null - : "Enter valid amount"), - _gap(), - _buildPayeeAutocompleteField(), - _gap(), - _buildDropdown( + key: _categoryDropdownKey, + ), + _gap(), + _buildTextField("Title", Icons.title_outlined, + controller.titleController, + hint: "Enter title", + validator: Validators.requiredField), + _gap(), + _buildRadio( + "Is Advance Payment", + Icons.attach_money_outlined, + controller.isAdvancePayment, + ["Yes", "No"]), + _gap(), + _buildDueDateField(), + _gap(), + _buildTextField("Amount", Icons.currency_rupee, + controller.amountController, + hint: "Enter Amount", + keyboardType: TextInputType.number, + validator: (v) => (v != null && + v.isNotEmpty && + double.tryParse(v) != null) + ? null + : "Enter valid amount"), + _gap(), + _buildPayeeAutocompleteField(), + _gap(), + _buildDropdown( "Currency", Icons.monetization_on_outlined, controller.selectedCurrency.value?.currencyName ?? @@ -209,16 +216,19 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> controller.currencies, (c) => c.currencyName, controller.selectCurrency, - key: _currencyDropdownKey), - _gap(), - _buildTextField("Description", Icons.description_outlined, - controller.descriptionController, - hint: "Enter description", - maxLines: 3, - validator: Validators.requiredField), - _gap(), - _buildAttachmentsSection(), - ], + key: _currencyDropdownKey, + ), + _gap(), + _buildTextField("Description", Icons.description_outlined, + controller.descriptionController, + hint: "Enter description", + maxLines: 3, + validator: Validators.requiredField), + _gap(), + _buildAttachmentsSection(), + MySpacing.height(30), + ], + ), ), ), ), @@ -284,6 +294,7 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> final i = entry.key; final label = entry.value; final value = i == 0; + return Expanded( child: RadioListTile( contentPadding: EdgeInsets.zero, @@ -354,7 +365,6 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> displayStringForOption: (option) => option, fieldViewBuilder: (context, fieldController, focusNode, onFieldSubmitted) { - // Avoid updating during build WidgetsBinding.instance.addPostFrameCallback((_) { if (fieldController.text != controller.selectedPayee.value) { fieldController.text = controller.selectedPayee.value; diff --git a/lib/view/directory/directory_main_screen.dart b/lib/view/directory/directory_main_screen.dart index 473efaf..9d44da0 100644 --- a/lib/view/directory/directory_main_screen.dart +++ b/lib/view/directory/directory_main_screen.dart @@ -99,34 +99,60 @@ class _DirectoryMainScreenState extends State ), ), ), - 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"), - ], + body: SafeArea( + child: 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(), - ], + // ---------------- TabBarView + Scroll / Landscape Support ---------------- + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = + constraints.maxWidth > constraints.maxHeight; + + if (isLandscape) { + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: SizedBox( + height: constraints.maxHeight * 1.3, + child: TabBarView( + controller: _tabController, + children: [ + DirectoryView(), + NotesView(), + ], + ), + ), + ); + } + + // Portrait + return 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..8e334ca 100644 --- a/lib/view/finance/advance_payment_screen.dart +++ b/lib/view/finance/advance_payment_screen.dart @@ -49,39 +49,72 @@ 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(), - ], + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = + constraints.maxWidth > constraints.maxHeight; + + return 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, + + // ---------------- PORTRAIT (UNCHANGED) ---------------- + child: !isLandscape + ? SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Container( + color: const Color(0xFFF5F5F5), + child: Column( + children: [ + _buildSearchBar(), + _buildEmployeeDropdown(context), + _buildTopBalance(), + _buildPaymentList(), + ], + ), + ), + ) + + // ---------------- LANDSCAPE (FIXED) ---------------- + : SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Container( + width: double.infinity, + color: const Color(0xFFF5F5F5), + + // โ— Removed IntrinsicHeight + // โ— Removed ConstrainedBox + // Dropdown can now open freely + + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildSearchBar(), + _buildEmployeeDropdown( + context), // now overlay works + _buildTopBalance(), + _buildPaymentList(), + ], + ), + ), + ), ), - ), - ), + ); + }, ), ), ); diff --git a/lib/view/finance/finance_screen.dart b/lib/view/finance/finance_screen.dart index 32cba67..b7142f0 100644 --- a/lib/view/finance/finance_screen.dart +++ b/lib/view/finance/finance_screen.dart @@ -113,171 +113,219 @@ class _FinanceScreenState extends State ), ), ), - body: FadeTransition( - opacity: _fadeAnimation, - child: Obx(() { - if (menuController.isLoading.value) { - return const Center(child: CircularProgressIndicator()); - } + body: SafeArea( + child: FadeTransition( + opacity: _fadeAnimation, + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = + constraints.maxWidth > constraints.maxHeight; - 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), - ), - ); - } + return Obx(() { + if (menuController.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } - // Filter allowed Finance menus dynamically - final financeMenuIds = [ - MenuItems.expenseReimbursement, - MenuItems.paymentRequests, - MenuItems.advancePaymentStatements, - ]; + 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 financeMenus = menuController.menuItems - .where((m) => financeMenuIds.contains(m.id) && m.available) - .toList(); + // Filter allowed Finance menus dynamically + final financeMenuIds = [ + MenuItems.expenseReimbursement, + MenuItems.paymentRequests, + MenuItems.advancePaymentStatements, + ]; - if (financeMenus.isEmpty) { - return const Center( - child: Text( - "You donโ€™t have access to the Finance section.", - style: TextStyle(color: Colors.grey), - ), - ); - } + final financeMenus = menuController.menuItems + .where((m) => financeMenuIds.contains(m.id) && m.available) + .toList(); - 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(), - ], - ), - ); - }), + if (financeMenus.isEmpty) { + return const Center( + child: Text( + "You donโ€™t have access to the Finance section.", + style: TextStyle(color: Colors.grey), + ), + ); + } + + // ---------------------- PORTRAIT MODE ---------------------- + if (!isLandscape) { + 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(), + ], + ), + ); + } + + // ---------------------- LANDSCAPE MODE ---------------------- + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _buildFinanceModulesCompact(financeMenus), + MySpacing.height(24), + + // Wider charts behave better side-by-side or full width + SizedBox( + width: constraints.maxWidth, + child: ExpenseByStatusWidget( + controller: dashboardController), + ), + MySpacing.height(24), + + SizedBox( + width: constraints.maxWidth, + child: ExpenseTypeReportChart(), + ), + MySpacing.height(24), + + SizedBox( + width: constraints.maxWidth, + child: 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..8fb2a0c 100644 --- a/lib/view/finance/payment_request_screen.dart +++ b/lib/view/finance/payment_request_screen.dart @@ -99,41 +99,76 @@ 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), - ], - ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = + constraints.maxWidth > constraints.maxHeight; + + return Column( + children: [ + // ---------------- TabBar ---------------- + 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"), + ], ), - ], - ), - ), - ), - ], + ), + + // ---------------- Content Area ---------------- + Expanded( + child: Container( + color: Colors.grey[100], + child: isLandscape + ? SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: SizedBox( + height: constraints.maxHeight * 1.3, + child: Column( + children: [ + _buildSearchBar(), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildPaymentRequestList( + isHistory: false), + _buildPaymentRequestList( + isHistory: true), + ], + ), + ), + ], + ), + ), + ) + : Column( + children: [ + _buildSearchBar(), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildPaymentRequestList(isHistory: false), + _buildPaymentRequestList(isHistory: true), + ], + ), + ), + ], + ), + ), + ), + ], + ); + }, + ), ), floatingActionButton: Obx(() { if (permissionController.permissions.isEmpty) {