marco.pms.mobileapp/lib/view/expense/expense_screen.dart
Vaibhav Surve 65fbef3441 Enhance UI and Navigation
- Added navigation to the dashboard after applying the theme in ThemeController.
- Introduced a new PillTabBar widget for a modern tab design across multiple screens.
- Updated dashboard screen to improve button actions and UI consistency.
- Refactored contact detail screen to streamline layout and enhance gradient effects.
- Implemented PillTabBar in directory main screen, expense screen, and payment request screen for consistent tab navigation.
- Improved layout structure in user document screen and employee profile screen for better user experience.
- Enhanced service project details screen with a modern tab bar implementation.
2025-11-28 14:48:39 +05:30

247 lines
8.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:on_field_work/controller/project_controller.dart';
import 'package:on_field_work/controller/expense/expense_screen_controller.dart';
import 'package:on_field_work/controller/permission_controller.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
import 'package:on_field_work/model/expense/expense_list_model.dart';
import 'package:on_field_work/model/expense/add_expense_bottom_sheet.dart';
import 'package:on_field_work/view/expense/expense_filter_bottom_sheet.dart';
import 'package:on_field_work/helpers/widgets/expense/expense_main_components.dart';
import 'package:on_field_work/helpers/utils/permission_constants.dart';
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
import 'package:on_field_work/helpers/widgets/pill_tab_bar.dart';
class ExpenseMainScreen extends StatefulWidget {
const ExpenseMainScreen({super.key});
@override
State<ExpenseMainScreen> createState() => _ExpenseMainScreenState();
}
class _ExpenseMainScreenState extends State<ExpenseMainScreen>
with SingleTickerProviderStateMixin, UIMixin {
late TabController _tabController;
final searchController = TextEditingController();
final expenseController = Get.put(ExpenseController());
final projectController = Get.find<ProjectController>();
final permissionController = Get.put(PermissionController());
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
// Defer expense fetch to after first build
WidgetsBinding.instance.addPostFrameCallback((_) {
expenseController.fetchExpenses();
});
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
Future<void> _refreshExpenses() async {
await expenseController.fetchExpenses();
}
void _openFilterBottomSheet() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => ExpenseFilterBottomSheet(
expenseController: expenseController,
scrollController: ScrollController(),
),
);
}
List<ExpenseModel> _getFilteredExpenses({required bool isHistory}) {
final query = searchController.text.trim().toLowerCase();
final now = DateTime.now();
final filtered = expenseController.expenses.where((e) {
return query.isEmpty ||
e.expenseCategory.name.toLowerCase().contains(query) ||
e.supplerName.toLowerCase().contains(query) ||
e.paymentMode.name.toLowerCase().contains(query);
}).toList()
..sort((a, b) => b.transactionDate.compareTo(a.transactionDate));
return isHistory
? filtered
.where((e) =>
e.transactionDate.isBefore(DateTime(now.year, now.month)))
.toList()
: filtered
.where((e) =>
e.transactionDate.month == now.month &&
e.transactionDate.year == now.year)
.toList();
}
@override
Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
backgroundColor: Colors.white,
appBar: CustomAppBar(
title: "Expense & Reimbursement",
backgroundColor: appBarColor,
onBackPressed: () => Get.toNamed('/dashboard/finance'),
),
body: Stack(
children: [
// === FULL GRADIENT BEHIND APPBAR & TABBAR ===
Positioned.fill(
child: Column(
children: [
Container(
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
),
),
),
Expanded(
child: Container(color: Colors.grey[100]),
),
],
),
),
// === MAIN CONTENT ===
SafeArea(
top: false,
bottom: true,
child: Column(
children: [
PillTabBar(
controller: _tabController,
tabs: const ["Current Month", "History"],
selectedColor: contentTheme.primary,
unselectedColor: Colors.grey.shade600,
indicatorColor: contentTheme.primary,
),
// CONTENT AREA
Expanded(
child: Container(
color: Colors.transparent,
child: Column(
children: [
// SEARCH & FILTER
Padding(
padding: const EdgeInsets.symmetric(horizontal: 0),
child: SearchAndFilter(
controller: searchController,
onChanged: (_) => setState(() {}),
onFilterTap: _openFilterBottomSheet,
expenseController: expenseController,
),
),
// TABBAR VIEW
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildExpenseList(isHistory: false),
_buildExpenseList(isHistory: true),
],
),
),
],
),
),
),
],
),
),
],
),
floatingActionButton: Obx(() {
if (permissionController.permissions.isEmpty)
return const SizedBox.shrink();
final canUpload =
permissionController.hasPermission(Permissions.expenseUpload);
return canUpload
? FloatingActionButton.extended(
backgroundColor: contentTheme.primary,
onPressed: showAddExpenseBottomSheet,
icon: const Icon(Icons.add, color: Colors.white),
label: const Text(
"Add Expense",
style: TextStyle(color: Colors.white, fontSize: 16),
),
)
: const SizedBox.shrink();
}),
);
}
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<ScrollNotification>(
onNotification: (scrollInfo) {
if (scrollInfo.metrics.pixels ==
scrollInfo.metrics.maxScrollExtent &&
!expenseController.isLoading.value) {
expenseController.loadMoreExpenses();
}
return false;
},
child: ExpenseList(
expenseList: filteredList,
onViewDetail: () => expenseController.fetchExpenses(),
),
),
);
});
}
}