chnaged make expense flow
This commit is contained in:
parent
1c253df5f9
commit
fd861b3adb
@ -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;
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}),
|
||||
],
|
||||
|
||||
@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user