chnaged make expense flow

This commit is contained in:
Vaibhav Surve 2025-11-10 12:08:07 +05:30
parent 1c253df5f9
commit fd861b3adb
7 changed files with 79 additions and 58 deletions

View File

@ -327,7 +327,8 @@ class PaymentRequestDetailController extends GetxController {
}
// --- Submit Expense ---
Future<bool> submitExpense() async {
Future<bool> submitExpense(
{required String statusId, String? comment}) async {
if (selectedPaymentMode.value == null) return false;
isSubmitting.value = true;
@ -355,6 +356,8 @@ class PaymentRequestDetailController extends GetxController {
gstNumber: gstNumberController.text,
paymentRequestId: _requestId,
billAttachments: attachmentsPayload,
statusId: statusId,
comment: comment ?? '',
);
} finally {
isSubmitting.value = false;

View File

@ -22,7 +22,7 @@ class ApiEndpoints {
static const String updateExpensePaymentRequestStatus =
"/Expense/payment-request/action";
static const String createExpenseforPR =
"/Expense/payment-request/expense/create";
"/expense/payment-request/action";
static const String getDashboardProjectProgress = "/dashboard/progression";
@ -96,7 +96,7 @@ class ApiEndpoints {
static const String editExpense = "/Expense/edit";
static const String getMasterPaymentModes = "/master/payment-modes";
static const String getMasterExpenseStatus = "/master/expenses-status";
static const String getMasterExpenseTypes = "/master/expenses-types";
static const String getMasterExpenseCategory = "/master/expenses-categories";
static const String updateExpenseStatus = "/expense/action";
static const String deleteExpense = "/expense/delete";

View File

@ -307,7 +307,9 @@ class ApiService {
required String paymentModeId,
required String location,
required String gstNumber,
required String statusId,
required String paymentRequestId,
required String comment,
List<Map<String, dynamic>> billAttachments = const [],
}) async {
const endpoint = ApiEndpoints.createExpenseforPR;
@ -316,6 +318,8 @@ class ApiService {
"paymentModeId": paymentModeId,
"location": location,
"gstNumber": gstNumber,
"statusId": statusId,
"comment": comment,
"paymentRequestId": paymentRequestId,
"billAttachments": billAttachments,
};
@ -789,7 +793,8 @@ class ApiService {
return null;
}
/// Create Project API
/// Create Project API
static Future<bool> createProjectApi({
required String name,
required String projectAddress,
@ -1830,7 +1835,7 @@ class ApiService {
/// Fetch Master Expense Types
static Future<List<dynamic>?> getMasterExpenseTypes() async {
const endpoint = ApiEndpoints.getMasterExpenseTypes;
const endpoint = ApiEndpoints.getMasterExpenseCategory;
return _getRequest(endpoint).then((res) => res != null
? _parseResponse(res, label: 'Master Expense Types')
: null);

View File

@ -9,6 +9,7 @@ import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/model/expense/expense_list_model.dart';
import 'package:marco/view/expense/expense_detail_screen.dart';
import 'package:marco/helpers/widgets/my_confirmation_dialog.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
class ExpenseAppBar extends StatelessWidget implements PreferredSizeWidget {
final ProjectController projectController;
@ -72,7 +73,7 @@ class ExpenseAppBar extends StatelessWidget implements PreferredSizeWidget {
}
}
class SearchAndFilter extends StatelessWidget {
class SearchAndFilter extends StatefulWidget {
final TextEditingController controller;
final ValueChanged<String> onChanged;
final VoidCallback onFilterTap;
@ -86,6 +87,11 @@ class SearchAndFilter extends StatelessWidget {
super.key,
});
@override
State<SearchAndFilter> createState() => _SearchAndFilterState();
}
class _SearchAndFilterState extends State<SearchAndFilter> with UIMixin {
@override
Widget build(BuildContext context) {
return Padding(
@ -96,8 +102,8 @@ class SearchAndFilter extends StatelessWidget {
child: SizedBox(
height: 35,
child: TextField(
controller: controller,
onChanged: onChanged,
controller: widget.controller,
onChanged: widget.onChanged,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
prefixIcon:
@ -106,11 +112,16 @@ class SearchAndFilter extends StatelessWidget {
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
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(10),
borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Colors.grey.shade300),
),
),
@ -124,7 +135,7 @@ class SearchAndFilter extends StatelessWidget {
clipBehavior: Clip.none,
children: [
const Icon(Icons.tune, color: Colors.black),
if (expenseController.isFilterApplied)
if (widget.expenseController.isFilterApplied)
Positioned(
top: -1,
right: -1,
@ -140,7 +151,7 @@ class SearchAndFilter extends StatelessWidget {
),
],
),
onPressed: onFilterTap,
onPressed: widget.onFilterTap,
);
}),
],

View File

@ -8,14 +8,18 @@ import 'package:marco/helpers/widgets/expense/expense_form_widgets.dart';
import 'package:marco/helpers/utils/validators.dart';
import 'package:marco/controller/finance/payment_request_detail_controller.dart';
Future<T?> showCreateExpenseBottomSheet<T>() {
Future<T?> showCreateExpenseBottomSheet<T>({required String statusId}) {
return Get.bottomSheet<T>(
_CreateExpenseBottomSheet(),
_CreateExpenseBottomSheet(statusId: statusId),
isScrollControlled: true,
);
}
class _CreateExpenseBottomSheet extends StatefulWidget {
final String statusId;
const _CreateExpenseBottomSheet({required this.statusId, Key? key})
: super(key: key);
@override
State<_CreateExpenseBottomSheet> createState() =>
_CreateExpenseBottomSheetState();
@ -24,8 +28,9 @@ class _CreateExpenseBottomSheet extends StatefulWidget {
class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> {
final controller = Get.put(PaymentRequestDetailController());
final _formKey = GlobalKey<FormState>();
final _paymentModeDropdownKey = GlobalKey();
final TextEditingController commentController = TextEditingController();
final _paymentModeDropdownKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Obx(
@ -37,7 +42,10 @@ class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> {
onCancel: Get.back,
onSubmit: () async {
if (_formKey.currentState!.validate() && _validateSelections()) {
final success = await controller.submitExpense();
final success = await controller.submitExpense(
statusId: widget.statusId,
comment: commentController.text.trim(),
);
if (success) {
Get.back();
showAppSnackbar(
@ -47,6 +55,7 @@ class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> {
);
}
}
;
},
child: SingleChildScrollView(
child: Column(
@ -67,7 +76,7 @@ class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> {
Icons.receipt_outlined,
controller.gstNumberController,
hint: "Enter GST Number",
validator: null, // optional field
validator: null,
),
_gap(),
_buildTextField(
@ -86,6 +95,15 @@ class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> {
),
_gap(),
_buildAttachmentField(),
_gap(),
_buildTextField(
"Comment",
Icons.comment_outlined,
commentController,
hint: "Enter a comment (optional)",
validator: null,
),
_gap(),
],
),
),
@ -131,7 +149,7 @@ class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> {
hint: hint ?? "",
validator: validator,
keyboardType: keyboardType ?? TextInputType.text,
suffixIcon: suffixIcon,
suffixIcon: suffixIcon,
),
],
);

View File

@ -118,8 +118,7 @@ class PaymentRequestData {
expenseStatus: ExpenseStatus.fromJson(json['expenseStatus']),
paidTransactionId: json['paidTransactionId'],
paidAt: json['paidAt'] != null ? DateTime.parse(json['paidAt']) : null,
paidBy:
json['paidBy'] != null ? User.fromJson(json['paidBy']) : null,
paidBy: json['paidBy'] != null ? User.fromJson(json['paidBy']) : null,
isAdvancePayment: json['isAdvancePayment'],
createdAt: DateTime.parse(json['createdAt']),
createdBy: User.fromJson(json['createdBy']),
@ -373,7 +372,7 @@ class NextStatus {
class UpdateLog {
String id;
ExpenseStatus status;
ExpenseStatus? status;
ExpenseStatus nextStatus;
String comment;
DateTime updatedAt;
@ -381,7 +380,7 @@ class UpdateLog {
UpdateLog({
required this.id,
required this.status,
this.status,
required this.nextStatus,
required this.comment,
required this.updatedAt,
@ -390,7 +389,9 @@ class UpdateLog {
factory UpdateLog.fromJson(Map<String, dynamic> json) => UpdateLog(
id: json['id'],
status: ExpenseStatus.fromJson(json['status']),
status: json['status'] != null
? ExpenseStatus.fromJson(json['status'])
: null,
nextStatus: ExpenseStatus.fromJson(json['nextStatus']),
comment: json['comment'],
updatedAt: DateTime.parse(json['updatedAt']),
@ -399,7 +400,7 @@ class UpdateLog {
Map<String, dynamic> toJson() => {
'id': id,
'status': status.toJson(),
'status': status?.toJson(),
'nextStatus': nextStatus.toJson(),
'comment': comment,
'updatedAt': updatedAt.toIso8601String(),

View File

@ -34,7 +34,7 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
with UIMixin {
final controller = Get.put(PaymentRequestDetailController());
final projectController = Get.find<ProjectController>();
final permissionController = Get.find<PermissionController>();
final permissionController = Get.put(PermissionController());
final RxBool canSubmit = false.obs;
bool _checkedPermission = false;
@ -163,36 +163,6 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
.hasAnyPermission(status.permissionIds ?? []);
}).toList();
// If there are no next statuses, show "Create Expense" button
if (availableStatuses.isEmpty) {
return SafeArea(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.grey.shade300)),
),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () {
showCreateExpenseBottomSheet();
},
child: const Text(
"Create Expense",
style: TextStyle(
color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
);
}
// Normal status buttons
return SafeArea(
child: Container(
@ -218,6 +188,7 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
),
),
onPressed: () async {
// If status is reimbursement, show reimbursement bottom sheet
if (status.id == reimbursementStatusId) {
showModalBottomSheet(
context: context,
@ -232,6 +203,15 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
onClose: () {},
),
);
// If status is b8586f67-dc19-49c3-b4af-224149efe1d3, open create expense
} else if (status.id ==
'b8586f67-dc19-49c3-b4af-224149efe1d3') {
showCreateExpenseBottomSheet(
statusId: status.id,
);
// Normal status flow
} else {
final comment = await showCommentBottomSheet(
context, status.displayName);
@ -407,8 +387,12 @@ class _Logs extends StatelessWidget {
itemBuilder: (_, index) {
final log = reversedLogs[index];
final status = log.status.name;
final description = log.status.description;
final status = log.status?.name ?? 'Unknown';
final description = log.status?.description ?? '';
final statusColor = log.status != null
? colorParser(log.status!.color)
: Colors.grey;
final comment = log.comment;
final nextStatusName = log.nextStatus.name;
@ -421,7 +405,6 @@ class _Logs extends StatelessWidget {
final timestamp = _parseTimestamp(log.updatedAt);
final timeAgo = timeago.format(timestamp);
final statusColor = colorParser(log.status.color);
final nextStatusColor = colorParser(log.nextStatus.color);
return TimelineTile(