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

View File

@ -22,7 +22,7 @@ class ApiEndpoints {
static const String updateExpensePaymentRequestStatus = static const String updateExpensePaymentRequestStatus =
"/Expense/payment-request/action"; "/Expense/payment-request/action";
static const String createExpenseforPR = static const String createExpenseforPR =
"/Expense/payment-request/expense/create"; "/expense/payment-request/action";
static const String getDashboardProjectProgress = "/dashboard/progression"; static const String getDashboardProjectProgress = "/dashboard/progression";
@ -96,7 +96,7 @@ class ApiEndpoints {
static const String editExpense = "/Expense/edit"; static const String editExpense = "/Expense/edit";
static const String getMasterPaymentModes = "/master/payment-modes"; static const String getMasterPaymentModes = "/master/payment-modes";
static const String getMasterExpenseStatus = "/master/expenses-status"; 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 updateExpenseStatus = "/expense/action";
static const String deleteExpense = "/expense/delete"; static const String deleteExpense = "/expense/delete";

View File

@ -307,7 +307,9 @@ class ApiService {
required String paymentModeId, required String paymentModeId,
required String location, required String location,
required String gstNumber, required String gstNumber,
required String statusId,
required String paymentRequestId, required String paymentRequestId,
required String comment,
List<Map<String, dynamic>> billAttachments = const [], List<Map<String, dynamic>> billAttachments = const [],
}) async { }) async {
const endpoint = ApiEndpoints.createExpenseforPR; const endpoint = ApiEndpoints.createExpenseforPR;
@ -316,6 +318,8 @@ class ApiService {
"paymentModeId": paymentModeId, "paymentModeId": paymentModeId,
"location": location, "location": location,
"gstNumber": gstNumber, "gstNumber": gstNumber,
"statusId": statusId,
"comment": comment,
"paymentRequestId": paymentRequestId, "paymentRequestId": paymentRequestId,
"billAttachments": billAttachments, "billAttachments": billAttachments,
}; };
@ -789,7 +793,8 @@ class ApiService {
return null; return null;
} }
/// Create Project API
/// Create Project API
static Future<bool> createProjectApi({ static Future<bool> createProjectApi({
required String name, required String name,
required String projectAddress, required String projectAddress,
@ -1830,7 +1835,7 @@ class ApiService {
/// Fetch Master Expense Types /// Fetch Master Expense Types
static Future<List<dynamic>?> getMasterExpenseTypes() async { static Future<List<dynamic>?> getMasterExpenseTypes() async {
const endpoint = ApiEndpoints.getMasterExpenseTypes; const endpoint = ApiEndpoints.getMasterExpenseCategory;
return _getRequest(endpoint).then((res) => res != null return _getRequest(endpoint).then((res) => res != null
? _parseResponse(res, label: 'Master Expense Types') ? _parseResponse(res, label: 'Master Expense Types')
: null); : 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/model/expense/expense_list_model.dart';
import 'package:marco/view/expense/expense_detail_screen.dart'; import 'package:marco/view/expense/expense_detail_screen.dart';
import 'package:marco/helpers/widgets/my_confirmation_dialog.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 { class ExpenseAppBar extends StatelessWidget implements PreferredSizeWidget {
final ProjectController projectController; 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 TextEditingController controller;
final ValueChanged<String> onChanged; final ValueChanged<String> onChanged;
final VoidCallback onFilterTap; final VoidCallback onFilterTap;
@ -86,6 +87,11 @@ class SearchAndFilter extends StatelessWidget {
super.key, super.key,
}); });
@override
State<SearchAndFilter> createState() => _SearchAndFilterState();
}
class _SearchAndFilterState extends State<SearchAndFilter> with UIMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
@ -96,8 +102,8 @@ class SearchAndFilter extends StatelessWidget {
child: SizedBox( child: SizedBox(
height: 35, height: 35,
child: TextField( child: TextField(
controller: controller, controller: widget.controller,
onChanged: onChanged, onChanged: widget.onChanged,
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 12), contentPadding: const EdgeInsets.symmetric(horizontal: 12),
prefixIcon: prefixIcon:
@ -106,11 +112,16 @@ class SearchAndFilter extends StatelessWidget {
filled: true, filled: true,
fillColor: Colors.white, fillColor: Colors.white,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Colors.grey.shade300), borderSide: BorderSide(color: Colors.grey.shade300),
), ),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide:
BorderSide(color: contentTheme.primary, width: 1.5),
),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Colors.grey.shade300), borderSide: BorderSide(color: Colors.grey.shade300),
), ),
), ),
@ -124,7 +135,7 @@ class SearchAndFilter extends StatelessWidget {
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
const Icon(Icons.tune, color: Colors.black), const Icon(Icons.tune, color: Colors.black),
if (expenseController.isFilterApplied) if (widget.expenseController.isFilterApplied)
Positioned( Positioned(
top: -1, top: -1,
right: -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/helpers/utils/validators.dart';
import 'package:marco/controller/finance/payment_request_detail_controller.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>( return Get.bottomSheet<T>(
_CreateExpenseBottomSheet(), _CreateExpenseBottomSheet(statusId: statusId),
isScrollControlled: true, isScrollControlled: true,
); );
} }
class _CreateExpenseBottomSheet extends StatefulWidget { class _CreateExpenseBottomSheet extends StatefulWidget {
final String statusId;
const _CreateExpenseBottomSheet({required this.statusId, Key? key})
: super(key: key);
@override @override
State<_CreateExpenseBottomSheet> createState() => State<_CreateExpenseBottomSheet> createState() =>
_CreateExpenseBottomSheetState(); _CreateExpenseBottomSheetState();
@ -24,8 +28,9 @@ class _CreateExpenseBottomSheet extends StatefulWidget {
class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> { class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> {
final controller = Get.put(PaymentRequestDetailController()); final controller = Get.put(PaymentRequestDetailController());
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _paymentModeDropdownKey = GlobalKey(); final TextEditingController commentController = TextEditingController();
final _paymentModeDropdownKey = GlobalKey();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx( return Obx(
@ -37,7 +42,10 @@ class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> {
onCancel: Get.back, onCancel: Get.back,
onSubmit: () async { onSubmit: () async {
if (_formKey.currentState!.validate() && _validateSelections()) { if (_formKey.currentState!.validate() && _validateSelections()) {
final success = await controller.submitExpense(); final success = await controller.submitExpense(
statusId: widget.statusId,
comment: commentController.text.trim(),
);
if (success) { if (success) {
Get.back(); Get.back();
showAppSnackbar( showAppSnackbar(
@ -47,6 +55,7 @@ class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> {
); );
} }
} }
;
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
@ -67,7 +76,7 @@ class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> {
Icons.receipt_outlined, Icons.receipt_outlined,
controller.gstNumberController, controller.gstNumberController,
hint: "Enter GST Number", hint: "Enter GST Number",
validator: null, // optional field validator: null,
), ),
_gap(), _gap(),
_buildTextField( _buildTextField(
@ -86,6 +95,15 @@ class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> {
), ),
_gap(), _gap(),
_buildAttachmentField(), _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 ?? "", hint: hint ?? "",
validator: validator, validator: validator,
keyboardType: keyboardType ?? TextInputType.text, keyboardType: keyboardType ?? TextInputType.text,
suffixIcon: suffixIcon, suffixIcon: suffixIcon,
), ),
], ],
); );

View File

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

View File

@ -34,7 +34,7 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
with UIMixin { with UIMixin {
final controller = Get.put(PaymentRequestDetailController()); final controller = Get.put(PaymentRequestDetailController());
final projectController = Get.find<ProjectController>(); final projectController = Get.find<ProjectController>();
final permissionController = Get.find<PermissionController>(); final permissionController = Get.put(PermissionController());
final RxBool canSubmit = false.obs; final RxBool canSubmit = false.obs;
bool _checkedPermission = false; bool _checkedPermission = false;
@ -163,36 +163,6 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
.hasAnyPermission(status.permissionIds ?? []); .hasAnyPermission(status.permissionIds ?? []);
}).toList(); }).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 // Normal status buttons
return SafeArea( return SafeArea(
child: Container( child: Container(
@ -218,6 +188,7 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
), ),
), ),
onPressed: () async { onPressed: () async {
// If status is reimbursement, show reimbursement bottom sheet
if (status.id == reimbursementStatusId) { if (status.id == reimbursementStatusId) {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@ -232,6 +203,15 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
onClose: () {}, 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 { } else {
final comment = await showCommentBottomSheet( final comment = await showCommentBottomSheet(
context, status.displayName); context, status.displayName);
@ -407,8 +387,12 @@ class _Logs extends StatelessWidget {
itemBuilder: (_, index) { itemBuilder: (_, index) {
final log = reversedLogs[index]; final log = reversedLogs[index];
final status = log.status.name; final status = log.status?.name ?? 'Unknown';
final description = log.status.description; final description = log.status?.description ?? '';
final statusColor = log.status != null
? colorParser(log.status!.color)
: Colors.grey;
final comment = log.comment; final comment = log.comment;
final nextStatusName = log.nextStatus.name; final nextStatusName = log.nextStatus.name;
@ -421,7 +405,6 @@ class _Logs extends StatelessWidget {
final timestamp = _parseTimestamp(log.updatedAt); final timestamp = _parseTimestamp(log.updatedAt);
final timeAgo = timeago.format(timestamp); final timeAgo = timeago.format(timestamp);
final statusColor = colorParser(log.status.color);
final nextStatusColor = colorParser(log.nextStatus.color); final nextStatusColor = colorParser(log.nextStatus.color);
return TimelineTile( return TimelineTile(