marco.pms.mobileapp/lib/view/finance/payment_request_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

412 lines
14 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/finance/payment_request_controller.dart';
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:on_field_work/model/finance/payment_request_filter_bottom_sheet.dart';
import 'package:on_field_work/model/finance/add_payment_request_bottom_sheet.dart';
import 'package:on_field_work/helpers/utils/date_time_utils.dart';
import 'package:on_field_work/view/finance/payment_request_detail_screen.dart';
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
import 'package:on_field_work/controller/permission_controller.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/widgets/custom_app_bar.dart';
import 'package:on_field_work/helpers/widgets/pill_tab_bar.dart';
class PaymentRequestMainScreen extends StatefulWidget {
const PaymentRequestMainScreen({super.key});
@override
State<PaymentRequestMainScreen> createState() =>
_PaymentRequestMainScreenState();
}
class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
with SingleTickerProviderStateMixin, UIMixin {
late TabController _tabController;
final searchController = TextEditingController();
final paymentController = Get.put(PaymentRequestController());
final projectController = Get.find<ProjectController>();
final permissionController = Get.put(PermissionController());
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
WidgetsBinding.instance.addPostFrameCallback((_) {
paymentController.fetchPaymentRequests();
});
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
Future<void> _refreshPaymentRequests() async {
await paymentController.fetchPaymentRequests();
}
void _openFilterBottomSheet() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => PaymentRequestFilterBottomSheet(
controller: paymentController,
scrollController: ScrollController(),
),
);
}
List filteredList({required bool isHistory}) {
final query = searchController.text.trim().toLowerCase();
final now = DateTime.now();
final filtered = paymentController.paymentRequests.where((e) {
final title = e.title?.toLowerCase() ?? "";
final payee = e.payee?.toLowerCase() ?? "";
return query.isEmpty || title.contains(query) || payee.contains(query);
}).toList()
..sort((a, b) {
final aDate = a.dueDate ?? DateTime(1900);
final bDate = b.dueDate ?? DateTime(1900);
return bDate.compareTo(aDate);
});
DateTime startOfMonth = DateTime(now.year, now.month, 1);
DateTime previousMonthEnd = DateTime(now.year, now.month, 0);
return isHistory
? filtered.where((e) {
final d = e.dueDate;
return d != null && d.isBefore(startOfMonth);
}).toList()
: filtered.where((e) {
final d = e.dueDate;
return d != null && d.isAfter(previousMonthEnd);
}).toList();
}
@override
Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
backgroundColor: Colors.white,
appBar: CustomAppBar(
title: "Payment Requests",
onBackPressed: () => Get.offNamed('/dashboard/finance'),
backgroundColor: appBarColor,
),
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: [
_buildSearchBar(),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildPaymentRequestList(isHistory: false),
_buildPaymentRequestList(isHistory: true),
],
),
),
],
),
),
),
],
),
),
],
),
floatingActionButton: Obx(() {
if (permissionController.permissions.isEmpty) {
return const SizedBox.shrink();
}
final canCreate =
permissionController.hasPermission(Permissions.expenseUpload);
return canCreate
? FloatingActionButton.extended(
backgroundColor: contentTheme.primary,
onPressed: showPaymentRequestBottomSheet,
icon: const Icon(Icons.add, color: Colors.white),
label: const Text(
"Create Payment Request",
style: TextStyle(color: Colors.white, fontSize: 16),
),
)
: const SizedBox.shrink();
}),
);
}
Widget _buildSearchBar() {
return Padding(
padding: MySpacing.fromLTRB(12, 10, 12, 0),
child: Row(
children: [
Expanded(
child: SizedBox(
height: 35,
child: TextField(
controller: searchController,
onChanged: (_) => setState(() {}),
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
prefixIcon:
const Icon(Icons.search, size: 20, color: Colors.grey),
hintText: 'Search payment requests...',
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide:
BorderSide(color: contentTheme.primary, width: 1.5),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Colors.grey.shade300),
),
),
),
),
),
MySpacing.width(4),
Obx(() {
return IconButton(
icon: Stack(
clipBehavior: Clip.none,
children: [
const Icon(Icons.tune, color: Colors.black),
if (paymentController.isFilterApplied.value)
Positioned(
top: -1,
right: -1,
child: Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 1.5),
),
),
),
],
),
onPressed: _openFilterBottomSheet,
);
}),
],
),
);
}
Widget _buildPaymentRequestList({required bool isHistory}) {
return Obx(() {
if (paymentController.isLoading.value &&
paymentController.paymentRequests.isEmpty) {
return SkeletonLoaders.paymentRequestListSkeletonLoader();
}
final list = filteredList(isHistory: isHistory);
final scrollController = ScrollController();
scrollController.addListener(() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 100 &&
!paymentController.isLoading.value) {
paymentController.loadMorePaymentRequests();
}
});
return MyRefreshIndicator(
onRefresh: _refreshPaymentRequests,
child: list.isEmpty
? ListView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.only(bottom: 100),
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.5,
child: Center(
child: Text(
paymentController.errorMessage.isNotEmpty
? paymentController.errorMessage.value
: "No payment requests found",
style: const TextStyle(color: Colors.grey),
),
),
),
],
)
: ListView.separated(
controller: scrollController,
// ------------------------
// 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),
itemBuilder: (context, index) {
if (index == list.length) {
return Obx(() => paymentController.isLoading.value
? const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(child: CircularProgressIndicator()),
)
: const SizedBox.shrink());
}
final item = list[index];
return _buildPaymentRequestTile(item);
},
),
);
});
}
Widget _buildPaymentRequestTile(dynamic item) {
final dueDate =
DateTimeUtils.formatDate(item.dueDate, DateTimeUtils.defaultFormat);
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {
Get.to(() => PaymentRequestDetailScreen(paymentRequestId: item.id));
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
MyText.bodyMedium(item.expenseCategory.name, fontWeight: 600),
if (item.isAdvancePayment == true) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.orange),
),
child: const Text(
"ADV",
style: TextStyle(
fontSize: 10,
color: Colors.orange,
fontWeight: FontWeight.bold,
),
),
),
],
],
),
const SizedBox(height: 6),
Row(
children: [
MyText.bodySmall("Payee: ", color: Colors.grey[600]),
MyText.bodySmall(item.payee, fontWeight: 600),
],
),
const SizedBox(height: 6),
Row(
children: [
Row(
children: [
MyText.bodySmall("Due Date: ", color: Colors.grey[600]),
MyText.bodySmall(dueDate, fontWeight: 600),
],
),
const Spacer(),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Color(int.parse(
'0xff${item.expenseStatus.color.substring(1)}'))
.withOpacity(0.5),
borderRadius: BorderRadius.circular(5),
),
child: MyText.bodySmall(
item.expenseStatus.name,
color: Colors.white,
fontWeight: 500,
),
),
],
),
],
),
),
),
);
}
}