marco.pms.mobileapp/lib/view/expense/expense_screen.dart
Vaibhav Surve 259f2aa928 Refactor UI components to use CustomAppBar and improve layout consistency
- Replaced existing AppBar implementations with CustomAppBar in multiple screens including PaymentRequestDetailScreen, PaymentRequestMainScreen, ServiceProjectDetailsScreen, JobDetailsScreen, DailyProgressReportScreen, DailyTaskPlanningScreen, and ServiceProjectScreen.
- Enhanced visual hierarchy by adding gradient backgrounds behind app bars for better aesthetics.
- Streamlined SafeArea usage to ensure proper content display across different devices.
- Improved code readability and maintainability by removing redundant code and consolidating UI elements.
2025-11-27 19:07:24 +05:30

255 lines
8.8 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';
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: [
// TAB BAR WITH TRANSPARENT BACKGROUND
Container(
decoration: const BoxDecoration(color: Colors.transparent),
child: TabBar(
controller: _tabController,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
indicatorColor: Colors.white,
indicatorWeight: 3,
tabs: const [
Tab(text: "Current Month"),
Tab(text: "History"),
],
),
),
// 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(),
),
),
);
});
}
}