feat: enhance job detail screen to display project branch information and improve payment request handling

This commit is contained in:
Vaibhav Surve 2025-11-20 16:21:41 +05:30
parent d0b40a9822
commit 87a7d19672
6 changed files with 86 additions and 15 deletions

View File

@ -13,6 +13,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart';
import 'package:mime/mime.dart';
import 'package:marco/controller/finance/payment_request_controller.dart';
class PaymentRequestDetailController extends GetxController {
final Rx<PaymentRequestData?> paymentRequest = Rx<PaymentRequestData?>(null);
@ -26,6 +27,8 @@ class PaymentRequestDetailController extends GetxController {
final RxList<EmployeeModel> employeeSearchResults = <EmployeeModel>[].obs;
final TextEditingController employeeSearchController =
TextEditingController();
PaymentRequestController get paymentRequestController =>
Get.find<PaymentRequestController>();
final RxBool isSearchingEmployees = false.obs;
// Attachments
@ -297,6 +300,7 @@ class PaymentRequestDetailController extends GetxController {
message: 'Payment submitted successfully',
type: SnackbarType.success);
await fetchPaymentRequestDetail();
paymentRequestController.fetchPaymentRequests();
} else {
showAppSnackbar(
title: 'Error',

View File

@ -305,7 +305,6 @@ class ExpenseList extends StatelessWidget {
onTap: () async {
final result = await Get.to(
() => ExpenseDetailScreen(expenseId: expense.id),
arguments: {'expense': expense},
);
if (result == true && onViewDetail != null) {
await onViewDetail!();

View File

@ -47,6 +47,7 @@ class JobData {
final User? createdBy;
final List<Tag>? tags;
final List<UpdateLog>? updateLogs;
final ProjectBranch? projectBranch;
JobData({
this.id,
@ -66,6 +67,7 @@ class JobData {
this.createdBy,
this.tags,
this.updateLogs,
this.projectBranch,
});
factory JobData.fromJson(Map<String, dynamic>? json) {
@ -76,7 +78,8 @@ class JobData {
title: json['title'] as String?,
description: json['description'] as String?,
jobTicketUId: json['jobTicketUId'] as String?,
project: json['project'] != null ? Project.fromJson(json['project']) : null,
project:
json['project'] != null ? Project.fromJson(json['project']) : null,
assignees: (json['assignees'] as List<dynamic>?)
?.map((e) => Assignee.fromJson(e))
.toList() ??
@ -89,7 +92,8 @@ class JobData {
attendanceId: json['attendanceId'] as String?,
nextTaggingAction: json['nextTaggingAction'] as int?,
createdAt: json['createdAt'] as String?,
createdBy: json['createdBy'] != null ? User.fromJson(json['createdBy']) : null,
createdBy:
json['createdBy'] != null ? User.fromJson(json['createdBy']) : null,
tags: (json['tags'] as List<dynamic>?)
?.map((e) => Tag.fromJson(e))
.toList() ??
@ -98,6 +102,9 @@ class JobData {
?.map((e) => UpdateLog.fromJson(e))
.toList() ??
[],
projectBranch: json['projectBranch'] != null
? ProjectBranch.fromJson(json['projectBranch'])
: null,
);
}
}
@ -170,6 +177,26 @@ class Assignee {
}
}
class ProjectBranch {
final String? id;
final String? branchName;
final String? branchType;
ProjectBranch({
this.id,
this.branchName,
this.branchType,
});
factory ProjectBranch.fromJson(Map<String, dynamic> json) {
return ProjectBranch(
id: json['id'],
branchName: json['branchName'],
branchType: json['branchType'],
);
}
}
class Status {
final String? id;
final String? name;
@ -266,9 +293,12 @@ class UpdateLog {
return UpdateLog(
id: json['id'] as String?,
status: json['status'] != null ? Status.fromJson(json['status']) : null,
nextStatus: json['nextStatus'] != null ? Status.fromJson(json['nextStatus']) : null,
nextStatus: json['nextStatus'] != null
? Status.fromJson(json['nextStatus'])
: null,
comment: json['comment'] as String?,
updatedBy: json['updatedBy'] != null ? User.fromJson(json['updatedBy']) : null,
updatedBy:
json['updatedBy'] != null ? User.fromJson(json['updatedBy']) : null,
);
}
}

View File

@ -12,6 +12,8 @@ import 'package:marco/view/finance/payment_request_detail_screen.dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/helpers/utils/permission_constants.dart';
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
class PaymentRequestMainScreen extends StatefulWidget {
const PaymentRequestMainScreen({super.key});
@ -283,10 +285,8 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
final list = filteredList(isHistory: isHistory);
// Single ScrollController for this list
// ScrollController for infinite scroll
final scrollController = ScrollController();
// Load more when reaching near bottom
scrollController.addListener(() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 100 &&
@ -295,7 +295,7 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
}
});
return RefreshIndicator(
return MyRefreshIndicator(
onRefresh: _refreshPaymentRequests,
child: list.isEmpty
? ListView(
@ -315,14 +315,13 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
],
)
: ListView.separated(
controller: scrollController, // attach controller
controller: scrollController,
padding: const EdgeInsets.fromLTRB(12, 12, 12, 80),
itemCount: list.length + 1, // extra item for loading
itemCount: list.length + 1,
separatorBuilder: (_, __) =>
Divider(color: Colors.grey.shade300, height: 20),
itemBuilder: (context, index) {
if (index == list.length) {
// Show loading indicator at bottom
return Obx(() => paymentController.isLoading.value
? const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
@ -330,7 +329,6 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
)
: const SizedBox.shrink());
}
final item = list[index];
return _buildPaymentRequestTile(item);
},

View File

@ -496,7 +496,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
onPressed: () async {
isAttendanceExpanded.value =
!isAttendanceExpanded.value;
if (isAttendanceExpanded.value ) {
if (isAttendanceExpanded.value) {
await controller
.fetchJobAttendanceLog(job.attendanceId ?? '');
}
@ -716,6 +716,40 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
});
}
Widget _rowItem(String label, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: MyText.bodySmall(label,
fontWeight: 600, color: Colors.grey.shade700),
),
Expanded(
flex: 5,
child: MyText.bodyMedium(value, fontWeight: 500),
),
],
);
}
Widget _branchDisplay() {
final job = controller.jobDetail.value?.data;
final branch = job?.projectBranch;
if (branch == null) {
return MyText.labelMedium("No branch assigned");
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_rowItem("Branch Name", branch.branchName ?? "-"),
MySpacing.height(8),
_rowItem("Branch Type", branch.branchType ?? "-"),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -765,6 +799,12 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
],
),
MySpacing.height(12),
_buildSectionCard(
title: "Project Branch",
titleIcon: Icons.account_tree_outlined,
children: [_branchDisplay()],
),
MySpacing.height(16),
_buildSectionCard(
title: "Assignees",
titleIcon: Icons.person_outline,