From 83d9d0689af11b41eacf36f72098e02cee6708f1 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 24 Sep 2025 17:16:38 +0530 Subject: [PATCH] feat: Implement tabbed navigation and enhance UI for expense and directory views --- .../directory/add_contact_bottom_sheet.dart | 6 +- .../user_document_filter_bottom_sheet.dart | 25 +-- lib/view/directory/directory_main_screen.dart | 158 ++++--------- lib/view/directory/directory_view.dart | 61 ++--- lib/view/directory/notes_view.dart | 58 +++-- lib/view/document/user_document_screen.dart | 45 ++-- lib/view/expense/expense_detail_screen.dart | 34 +-- lib/view/expense/expense_screen.dart | 210 +++++++++++------- 8 files changed, 298 insertions(+), 299 deletions(-) diff --git a/lib/model/directory/add_contact_bottom_sheet.dart b/lib/model/directory/add_contact_bottom_sheet.dart index c8ef83a..943ac2e 100644 --- a/lib/model/directory/add_contact_bottom_sheet.dart +++ b/lib/model/directory/add_contact_bottom_sheet.dart @@ -412,12 +412,12 @@ class _AddContactBottomSheetState extends State { MySpacing.height(16), _textField("Organization", orgCtrl, required: true), MySpacing.height(16), - MyText.labelMedium("Select Bucket"), + MyText.labelMedium(" Bucket"), MySpacing.height(8), Stack( children: [ _popupSelector(controller.selectedBucket, controller.buckets, - "Select Bucket"), + "Choose Bucket"), Positioned( left: 0, right: 0, @@ -481,7 +481,7 @@ class _AddContactBottomSheetState extends State { MyText.labelMedium("Category"), MySpacing.height(8), _popupSelector(controller.selectedCategory, - controller.categories, "Select Category"), + controller.categories, "Choose Category"), MySpacing.height(16), MyText.labelMedium("Tags"), MySpacing.height(8), diff --git a/lib/model/document/user_document_filter_bottom_sheet.dart b/lib/model/document/user_document_filter_bottom_sheet.dart index a7899fa..fa0546a 100644 --- a/lib/model/document/user_document_filter_bottom_sheet.dart +++ b/lib/model/document/user_document_filter_bottom_sheet.dart @@ -34,6 +34,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { return BaseBottomSheet( title: 'Filter Documents', + submitText: 'Apply', showButtons: hasFilters, onCancel: () => Get.back(), onSubmit: () { @@ -108,7 +109,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { ), child: Center( child: MyText( - "Uploaded On", + "Upload Date", style: MyTextStyle.bodyMedium( color: docController.isUploadedAt.value @@ -139,7 +140,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { ), child: Center( child: MyText( - "Updated On", + "Update Date", style: MyTextStyle.bodyMedium( color: !docController .isUploadedAt.value @@ -165,7 +166,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { child: Obx(() { return _dateButton( label: docController.startDate.value == null - ? 'Start Date' + ? 'From Date' : DateTimeUtils.formatDate( DateTime.parse( docController.startDate.value!), @@ -191,7 +192,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { child: Obx(() { return _dateButton( label: docController.endDate.value == null - ? 'End Date' + ? 'To Date' : DateTimeUtils.formatDate( DateTime.parse( docController.endDate.value!), @@ -222,39 +223,35 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { _multiSelectField( label: "Uploaded By", items: filterData.uploadedBy, - fallback: "Select Uploaded By", + fallback: "Choose Uploaded By", selectedValues: docController.selectedUploadedBy, ), _multiSelectField( label: "Category", items: filterData.documentCategory, - fallback: "Select Category", + fallback: "Choose Category", selectedValues: docController.selectedCategory, ), _multiSelectField( label: "Type", items: filterData.documentType, - fallback: "Select Type", + fallback: "Choose Type", selectedValues: docController.selectedType, ), _multiSelectField( label: "Tag", items: filterData.documentTag, - fallback: "Select Tag", + fallback: "Choose Tag", selectedValues: docController.selectedTag, ), // --- Document Status --- _buildField( - "Select Document Status", + " Document Status", Obx(() { return Container( padding: MySpacing.all(12), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey.shade300), - ), + child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/view/directory/directory_main_screen.dart b/lib/view/directory/directory_main_screen.dart index b8b1fdc..c5d55a1 100644 --- a/lib/view/directory/directory_main_screen.dart +++ b/lib/view/directory/directory_main_screen.dart @@ -10,16 +10,36 @@ import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/view/directory/directory_view.dart'; import 'package:marco/view/directory/notes_view.dart'; -class DirectoryMainScreen extends StatelessWidget { - DirectoryMainScreen({super.key}); +class DirectoryMainScreen extends StatefulWidget { + const DirectoryMainScreen({super.key}); + + @override + State createState() => _DirectoryMainScreenState(); +} + +class _DirectoryMainScreenState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; final DirectoryController controller = Get.put(DirectoryController()); final NotesController notesController = Get.put(NotesController()); + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.white, + backgroundColor: const Color(0xFFF5F5F5), appBar: PreferredSize( preferredSize: const Size.fromHeight(72), child: AppBar( @@ -79,116 +99,34 @@ class DirectoryMainScreen extends StatelessWidget { ), ), ), - body: SafeArea( - child: Column( - children: [ - // Toggle between Directory and Notes - Padding( - padding: MySpacing.fromLTRB(8, 12, 8, 5), - child: Obx(() { - final isNotesView = controller.isNotesView.value; - - return Container( - padding: EdgeInsets.all(2), - 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: [ - Expanded( - child: GestureDetector( - onTap: () => controller.isNotesView.value = false, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.symmetric( - vertical: 6, horizontal: 10), - decoration: BoxDecoration( - color: !isNotesView - ? Colors.red - : Colors.transparent, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.contacts, - size: 16, - color: !isNotesView - ? Colors.white - : Colors.grey), - const SizedBox(width: 6), - Text( - 'Directory', - style: TextStyle( - color: !isNotesView - ? Colors.white - : Colors.grey, - fontWeight: FontWeight.w600, - fontSize: 13, - ), - ), - ], - ), - ), - ), - ), - Expanded( - child: GestureDetector( - onTap: () => controller.isNotesView.value = true, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.symmetric( - vertical: 6, horizontal: 10), - decoration: BoxDecoration( - color: - isNotesView ? Colors.red : Colors.transparent, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.notes, - size: 16, - color: isNotesView - ? Colors.white - : Colors.grey), - const SizedBox(width: 6), - Text( - 'Notes', - style: TextStyle( - color: isNotesView - ? Colors.white - : Colors.grey, - fontWeight: FontWeight.w600, - fontSize: 13, - ), - ), - ], - ), - ), - ), - ), - ], - ), - ); - }), + 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"), + ], ), + ), - // Main View - Expanded( - child: Obx(() => - controller.isNotesView.value ? NotesView() : DirectoryView()), + // ---------------- TabBarView ---------------- + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + DirectoryView(), + NotesView(), + ], ), - ], - ), + ), + ], ), ); } diff --git a/lib/view/directory/directory_view.dart b/lib/view/directory/directory_view.dart index 35fca78..5e2d0c4 100644 --- a/lib/view/directory/directory_view.dart +++ b/lib/view/directory/directory_view.dart @@ -144,15 +144,38 @@ class _DirectoryViewState extends State { ); } + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.perm_contact_cal, size: 60, color: Colors.grey), + MySpacing.height(18), + MyText.titleMedium( + 'No matching contacts found.', + fontWeight: 600, + color: Colors.grey, + ), + MySpacing.height(10), + MyText.bodySmall( + 'Try adjusting your filters or refresh to reload.', + color: Colors.grey, + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.white, - floatingActionButton: FloatingActionButton( + backgroundColor: Colors.grey[100], + floatingActionButton: FloatingActionButton.extended( heroTag: 'createContact', backgroundColor: Colors.red, onPressed: _handleCreateContact, - child: const Icon(Icons.person_add_alt_1, color: Colors.white), + icon: const Icon(Icons.person_add_alt_1, color: Colors.white), + label: const Text("Add Contact", style: TextStyle(color: Colors.white)), ), body: Column( children: [ @@ -195,11 +218,11 @@ class _DirectoryViewState extends State { filled: true, fillColor: Colors.white, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: Colors.grey.shade300), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: Colors.grey.shade300), ), ), @@ -217,7 +240,7 @@ class _DirectoryViewState extends State { decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(5), ), child: IconButton( icon: Icon(Icons.tune, @@ -262,14 +285,14 @@ class _DirectoryViewState extends State { decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(5), ), child: PopupMenuButton( padding: EdgeInsets.zero, icon: const Icon(Icons.more_vert, size: 20, color: Colors.black87), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(5), ), itemBuilder: (context) { List> menuItems = []; @@ -412,27 +435,7 @@ class _DirectoryViewState extends State { SkeletonLoaders.contactSkeletonCard(), ) : controller.filteredContacts.isEmpty - ? ListView( - physics: const AlwaysScrollableScrollPhysics(), - children: [ - SizedBox( - height: - MediaQuery.of(context).size.height * 0.6, - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.contact_page_outlined, - size: 60, color: Colors.grey), - const SizedBox(height: 12), - MyText.bodyMedium('No contacts found.', - fontWeight: 500), - ], - ), - ), - ), - ], - ) + ? _buildEmptyState() : ListView.separated( physics: const AlwaysScrollableScrollPhysics(), padding: MySpacing.only( diff --git a/lib/view/directory/notes_view.dart b/lib/view/directory/notes_view.dart index 9f6829f..2564584 100644 --- a/lib/view/directory/notes_view.dart +++ b/lib/view/directory/notes_view.dart @@ -71,6 +71,28 @@ class NotesView extends StatelessWidget { return buffer.toString(); } + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.perm_contact_cal, size: 60, color: Colors.grey), + MySpacing.height(18), + MyText.titleMedium( + 'No matching notes found.', + fontWeight: 600, + color: Colors.grey, + ), + MySpacing.height(10), + MyText.bodySmall( + 'Try adjusting your filters or refresh to reload.', + color: Colors.grey, + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { return Column( @@ -94,17 +116,17 @@ class NotesView extends StatelessWidget { filled: true, fillColor: Colors.white, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: Colors.grey.shade300), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: Colors.grey.shade300), ), ), ), ), - ), + ), ], ), ), @@ -121,25 +143,19 @@ class NotesView extends StatelessWidget { if (notes.isEmpty) { return MyRefreshIndicator( onRefresh: _refreshNotes, - child: ListView( - physics: const AlwaysScrollableScrollPhysics(), - children: [ - SizedBox( - height: MediaQuery.of(context).size.height * 0.6, - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.note_alt_outlined, - size: 60, color: Colors.grey), - const SizedBox(height: 12), - MyText.bodyMedium('No notes found.', - fontWeight: 500), - ], + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: + BoxConstraints(minHeight: constraints.maxHeight), + child: Center( + child: _buildEmptyState(), ), ), - ), - ], + ); + }, ), ); } @@ -193,7 +209,7 @@ class NotesView extends StatelessWidget { isEditing ? Colors.indigo : Colors.grey.shade300, width: 1.1, ), - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(5), boxShadow: const [ BoxShadow( color: Colors.black12, diff --git a/lib/view/document/user_document_screen.dart b/lib/view/document/user_document_screen.dart index caafcf3..b254f45 100644 --- a/lib/view/document/user_document_screen.dart +++ b/lib/view/document/user_document_screen.dart @@ -98,7 +98,7 @@ class _UserDocumentsPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(5), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), @@ -114,7 +114,7 @@ class _UserDocumentsPageState extends State { padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(5), ), child: const Icon(Icons.description, color: Colors.blue), ), @@ -190,7 +190,7 @@ class _UserDocumentsPageState extends State { if (result == true) { debugPrint("✅ Document deleted and removed from list"); } - } else if (value == "activate") { + } else if (value == "restore") { // existing activate flow (unchanged) final success = await docController.toggleDocumentActive( doc.id, @@ -201,14 +201,14 @@ class _UserDocumentsPageState extends State { if (success) { showAppSnackbar( - title: "Reactivated", - message: "Document reactivated successfully", + title: "Restored", + message: "Document reastored successfully", type: SnackbarType.success, ); } else { showAppSnackbar( title: "Error", - message: "Failed to reactivate document", + message: "Failed to restore document", type: SnackbarType.error, ); } @@ -226,8 +226,8 @@ class _UserDocumentsPageState extends State { permissionController .hasPermission(Permissions.modifyDocument)) const PopupMenuItem( - value: "activate", - child: Text("Activate"), + value: "restore", + child: Text("Restore"), ), ], ), @@ -307,11 +307,11 @@ class _UserDocumentsPageState extends State { filled: true, fillColor: Colors.white, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: Colors.grey.shade300), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: Colors.grey.shade300), ), ), @@ -331,7 +331,7 @@ class _UserDocumentsPageState extends State { decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(5), ), child: IconButton( padding: EdgeInsets.zero, @@ -345,9 +345,10 @@ class _UserDocumentsPageState extends State { showModalBottomSheet( context: context, isScrollControlled: true, + shape: const RoundedRectangleBorder( borderRadius: - BorderRadius.vertical(top: Radius.circular(20)), + BorderRadius.vertical(top: Radius.circular(5)), ), builder: (_) => UserDocumentFilterBottomSheet( entityId: resolvedEntityId, @@ -382,14 +383,14 @@ class _UserDocumentsPageState extends State { decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(5), ), child: PopupMenuButton( padding: EdgeInsets.zero, icon: const Icon(Icons.more_vert, size: 20, color: Colors.black87), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(5), ), itemBuilder: (context) => [ const PopupMenuItem( @@ -411,7 +412,7 @@ class _UserDocumentsPageState extends State { const Icon(Icons.visibility_off_outlined, size: 20, color: Colors.black87), const SizedBox(width: 10), - const Expanded(child: Text('Show Inactive')), + const Expanded(child: Text('Show Deleted Documents')), Switch.adaptive( value: docController.showInactive.value, activeColor: Colors.indigo, @@ -439,24 +440,24 @@ class _UserDocumentsPageState extends State { Widget _buildStatusHeader() { return Obx(() { final isInactive = docController.showInactive.value; + if (!isInactive) return const SizedBox.shrink(); // hide when active + return Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - color: isInactive ? Colors.red.shade50 : Colors.green.shade50, + color: Colors.red.shade50, child: Row( children: [ Icon( - isInactive ? Icons.visibility_off : Icons.check_circle, - color: isInactive ? Colors.red : Colors.green, + Icons.visibility_off, + color: Colors.red, size: 18, ), const SizedBox(width: 8), Text( - isInactive - ? "Showing Inactive Documents" - : "Showing Active Documents", + "Showing Deleted Documents", style: TextStyle( - color: isInactive ? Colors.red : Colors.green, + color: Colors.red, fontWeight: FontWeight.w600, ), ), diff --git a/lib/view/expense/expense_detail_screen.dart b/lib/view/expense/expense_detail_screen.dart index 0d6d100..ecb5b6e 100644 --- a/lib/view/expense/expense_detail_screen.dart +++ b/lib/view/expense/expense_detail_screen.dart @@ -21,6 +21,7 @@ import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/model/employees/employee_info.dart'; import 'package:timeline_tile/timeline_tile.dart'; + class ExpenseDetailScreen extends StatefulWidget { final String expenseId; const ExpenseDetailScreen({super.key, required this.expenseId}); @@ -105,7 +106,7 @@ class _ExpenseDetailScreenState extends State { constraints: const BoxConstraints(maxWidth: 520), child: Card( shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), + borderRadius: BorderRadius.circular(5)), elevation: 3, child: Padding( padding: const EdgeInsets.symmetric( @@ -123,14 +124,12 @@ class _ExpenseDetailScreenState extends State { const Divider(height: 30, thickness: 1.2), _InvoiceDocuments(documents: expense.documents), const Divider(height: 30, thickness: 1.2), - _InvoiceTotals( expense: expense, formattedAmount: formattedAmount, statusColor: statusColor, ), const Divider(height: 30, thickness: 1.2), - ], ), ), @@ -160,7 +159,7 @@ class _ExpenseDetailScreenState extends State { return const SizedBox.shrink(); } - return FloatingActionButton( + return FloatingActionButton.extended( onPressed: () async { final editData = { 'id': expense.id, @@ -197,8 +196,9 @@ class _ExpenseDetailScreenState extends State { await controller.fetchExpenseDetails(); }, backgroundColor: Colors.red, - tooltip: 'Edit Expense', - child: const Icon(Icons.edit), + icon: const Icon(Icons.edit), + label: MyText.bodyMedium( + "Edit Expense", fontWeight: 600, color: Colors.white), ); }), bottomNavigationBar: Obx(() { @@ -271,7 +271,7 @@ class _ExpenseDetailScreenState extends State { minimumSize: const Size(100, 40), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), backgroundColor: buttonColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), ), onPressed: () async { const reimbursementId = 'f18c5cfd-7815-4341-8da2-2c2d65778e27'; @@ -280,7 +280,7 @@ class _ExpenseDetailScreenState extends State { context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16))), + borderRadius: BorderRadius.vertical(top: Radius.circular(5))), builder: (context) => ReimbursementBottomSheet( expenseId: expense.id, statusId: next.id, @@ -470,7 +470,7 @@ class _InvoiceHeader extends StatelessWidget { Container( decoration: BoxDecoration( color: statusColor.withOpacity(0.15), - borderRadius: BorderRadius.circular(8)), + borderRadius: BorderRadius.circular(5)), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), child: Row( children: [ @@ -604,7 +604,7 @@ class _InvoiceDocuments extends StatelessWidget { const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(6), + borderRadius: BorderRadius.circular(5), color: Colors.grey.shade100, ), child: Row( @@ -679,7 +679,8 @@ class InvoiceLogs extends StatelessWidget { ), ), ), - beforeLineStyle: LineStyle(color: Colors.grey.shade300, thickness: 2), + beforeLineStyle: + LineStyle(color: Colors.grey.shade300, thickness: 2), endChild: Padding( padding: const EdgeInsets.all(12.0), child: Column( @@ -698,17 +699,20 @@ class InvoiceLogs extends StatelessWidget { const SizedBox(height: 8), Row( children: [ - Icon(Icons.access_time, size: 14, color: Colors.grey[600]), + Icon(Icons.access_time, + size: 14, color: Colors.grey[600]), const SizedBox(width: 4), - MyText.bodySmall(formattedDate, color: Colors.grey[700]), + MyText.bodySmall(formattedDate, + color: Colors.grey[700]), ], ), const SizedBox(height: 8), Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 4), decoration: BoxDecoration( color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(6), + borderRadius: BorderRadius.circular(5), ), child: MyText.bodySmall( log.action, diff --git a/lib/view/expense/expense_screen.dart b/lib/view/expense/expense_screen.dart index e7ce5e6..cf120f6 100644 --- a/lib/view/expense/expense_screen.dart +++ b/lib/view/expense/expense_screen.dart @@ -20,8 +20,9 @@ class ExpenseMainScreen extends StatefulWidget { State createState() => _ExpenseMainScreenState(); } -class _ExpenseMainScreenState extends State { - bool isHistoryView = false; +class _ExpenseMainScreenState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; final searchController = TextEditingController(); final expenseController = Get.put(ExpenseController()); final projectController = Get.find(); @@ -30,9 +31,16 @@ class _ExpenseMainScreenState extends State { @override void initState() { super.initState(); + _tabController = TabController(length: 2, vsync: this); expenseController.fetchExpenses(); } + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + Future _refreshExpenses() async { await expenseController.fetchExpenses(); } @@ -49,7 +57,7 @@ class _ExpenseMainScreenState extends State { ); } - List _getFilteredExpenses() { + List _getFilteredExpenses({required bool isHistory}) { final query = searchController.text.trim().toLowerCase(); final now = DateTime.now(); @@ -61,7 +69,7 @@ class _ExpenseMainScreenState extends State { }).toList() ..sort((a, b) => b.transactionDate.compareTo(a.transactionDate)); - return isHistoryView + return isHistory ? filtered .where((e) => e.transactionDate.isBefore(DateTime(now.year, now.month))) @@ -72,89 +80,121 @@ class _ExpenseMainScreenState extends State { e.transactionDate.year == now.year) .toList(); } - + @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - appBar: ExpenseAppBar(projectController: projectController), - body: SafeArea( - child: Column( - children: [ - SearchAndFilter( - controller: searchController, - onChanged: (_) => setState(() {}), - onFilterTap: _openFilterBottomSheet, - expenseController: expenseController, - ), - ToggleButtonsRow( - isHistoryView: isHistoryView, - onToggle: (v) => setState(() => isHistoryView = v), - ), - Expanded( - child: Obx(() { - // Loader while fetching first time - if (expenseController.isLoading.value && - expenseController.expenses.isEmpty) { - return SkeletonLoaders.expenseListSkeletonLoader(); - } - - final filteredList = _getFilteredExpenses(); - - return MyRefreshIndicator( - onRefresh: _refreshExpenses, - child: filteredList.isEmpty - ? ListView( - physics: - const AlwaysScrollableScrollPhysics(), - children: [ - SizedBox( - height: MediaQuery.of(context).size.height * 0.5, - child: Center( - child: MyText.bodyMedium( - expenseController.errorMessage.isNotEmpty - ? expenseController.errorMessage.value - : "No expenses found", - color: - expenseController.errorMessage.isNotEmpty - ? Colors.red - : Colors.grey, - ), - ), - ), - ], - ) - : NotificationListener( - onNotification: (scrollInfo) { - if (scrollInfo.metrics.pixels == - scrollInfo.metrics.maxScrollExtent && - !expenseController.isLoading.value) { - expenseController.loadMoreExpenses(); - } - return false; - }, - child: ExpenseList( - expenseList: filteredList, - onViewDetail: () => - expenseController.fetchExpenses(), - ), - ), - ); - }), - ) - ], +Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: ExpenseAppBar(projectController: projectController), + 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: "Current Month"), + Tab(text: "History"), + ], + ), ), - ), - // ✅ FAB only if user has expenseUpload permission - floatingActionButton: - permissionController.hasPermission(Permissions.expenseUpload) - ? FloatingActionButton( - backgroundColor: Colors.red, - onPressed: showAddExpenseBottomSheet, - child: const Icon(Icons.add, color: Colors.white), - ) - : null, - ); + // ---------------- Gray background for rest ---------------- + Expanded( + child: Container( + color: Colors.grey[100], // Light gray background + child: Column( + children: [ + // ---------------- Search ---------------- + Padding( + padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0), + child: SearchAndFilter( + controller: searchController, + onChanged: (_) => setState(() {}), + onFilterTap: _openFilterBottomSheet, + expenseController: expenseController, + ), + ), + + // ---------------- TabBarView ---------------- + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildExpenseList(isHistory: false), + _buildExpenseList(isHistory: true), + ], + ), + ), + ], + ), + ), + ), + ], + ), + + floatingActionButton: + permissionController.hasPermission(Permissions.expenseUpload) + ? FloatingActionButton( + backgroundColor: Colors.red, + onPressed: showAddExpenseBottomSheet, + child: const Icon(Icons.add, color: Colors.white), + ) + : null, + ); +} + + + Widget _buildExpenseList({required bool isHistory}) { + return Obx(() { + if (expenseController.isLoading.value && + expenseController.expenses.isEmpty) { + return SkeletonLoaders.expenseListSkeletonLoader(); + } + + final filteredList = _getFilteredExpenses(isHistory: isHistory); + + return MyRefreshIndicator( + onRefresh: _refreshExpenses, + child: filteredList.isEmpty + ? ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.5, + child: Center( + child: MyText.bodyMedium( + expenseController.errorMessage.isNotEmpty + ? expenseController.errorMessage.value + : "No expenses found", + color: expenseController.errorMessage.isNotEmpty + ? Colors.red + : Colors.grey, + ), + ), + ), + ], + ) + : NotificationListener( + onNotification: (scrollInfo) { + if (scrollInfo.metrics.pixels == + scrollInfo.metrics.maxScrollExtent && + !expenseController.isLoading.value) { + expenseController.loadMoreExpenses(); + } + return false; + }, + child: ExpenseList( + expenseList: filteredList, + onViewDetail: () => expenseController.fetchExpenses(), + ), + ), + ); + }); } } +