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

View File

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

View File

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

@ -26,7 +26,7 @@ class PaymentRequestDetailScreen extends StatefulWidget {
final String paymentRequestId; final String paymentRequestId;
const PaymentRequestDetailScreen({super.key, required this.paymentRequestId}); const PaymentRequestDetailScreen({super.key, required this.paymentRequestId});
@override @override
State<PaymentRequestDetailScreen> createState() => State<PaymentRequestDetailScreen> createState() =>
_PaymentRequestDetailScreenState(); _PaymentRequestDetailScreenState();
} }

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

View File

@ -496,7 +496,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
onPressed: () async { onPressed: () async {
isAttendanceExpanded.value = isAttendanceExpanded.value =
!isAttendanceExpanded.value; !isAttendanceExpanded.value;
if (isAttendanceExpanded.value ) { if (isAttendanceExpanded.value) {
await controller await controller
.fetchJobAttendanceLog(job.attendanceId ?? ''); .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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -765,6 +799,12 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
], ],
), ),
MySpacing.height(12), MySpacing.height(12),
_buildSectionCard(
title: "Project Branch",
titleIcon: Icons.account_tree_outlined,
children: [_branchDisplay()],
),
MySpacing.height(16),
_buildSectionCard( _buildSectionCard(
title: "Assignees", title: "Assignees",
titleIcon: Icons.person_outline, titleIcon: Icons.person_outline,