added payment request code

This commit is contained in:
Vaibhav Surve 2025-11-05 17:30:07 +05:30
parent b857c4d8bc
commit ac01ee8e47
10 changed files with 969 additions and 9 deletions

View File

@ -0,0 +1,260 @@
import 'dart:io';
import 'dart:convert';
import 'package:get/get.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mime/mime.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart';
import 'package:marco/model/finance/expense_category_model.dart';
import 'package:marco/model/finance/currency_list_model.dart';
class PaymentRequestController extends GetxController {
//
// 🔹 Loading States
//
final isLoadingPayees = false.obs;
final isLoadingCategories = false.obs;
final isLoadingCurrencies = false.obs;
final isProcessingAttachment = false.obs;
//
// 🔹 Data Lists
//
final payees = <String>[].obs;
final categories = <ExpenseCategory>[].obs;
final currencies = <Currency>[].obs;
final globalProjects = <String>[].obs;
//
// 🔹 Selected Values
//
final selectedCategory = Rx<ExpenseCategory?>(null);
final selectedPayee = ''.obs;
final selectedCurrency = Rx<Currency?>(null);
final selectedProject = ''.obs;
final isAdvancePayment = false.obs;
//
// 🔹 Text Controllers
//
final titleController = TextEditingController();
final dueDateController = TextEditingController();
final amountController = TextEditingController();
final descriptionController = TextEditingController();
//
// 🔹 Attachments
//
final attachments = <File>[].obs;
final existingAttachments = <Map<String, dynamic>>[].obs;
final ImagePicker _picker = ImagePicker();
//
// 🔹 Lifecycle
//
@override
void onInit() {
super.onInit();
fetchAllMasterData();
fetchGlobalProjects();
}
@override
void onClose() {
titleController.dispose();
dueDateController.dispose();
amountController.dispose();
descriptionController.dispose();
super.onClose();
}
//
// 🔹 Master Data Fetch
//
Future<void> fetchAllMasterData() async {
await Future.wait([
fetchPayees(),
fetchExpenseCategories(),
fetchCurrencies(),
]);
}
Future<void> fetchPayees() async {
try {
isLoadingPayees.value = true;
final response = await ApiService.getExpensePaymentRequestPayeeApi();
if (response != null && response.data.isNotEmpty) {
payees.value = response.data;
} else {
payees.clear();
}
} catch (e) {
logSafe("Error fetching payees: $e", level: LogLevel.error);
} finally {
isLoadingPayees.value = false;
}
}
Future<void> fetchExpenseCategories() async {
try {
isLoadingCategories.value = true;
final response = await ApiService.getMasterExpenseCategoriesApi();
if (response != null && response.data.isNotEmpty) {
categories.value = response.data;
} else {
categories.clear();
}
} catch (e) {
logSafe("Error fetching categories: $e", level: LogLevel.error);
} finally {
isLoadingCategories.value = false;
}
}
Future<void> fetchCurrencies() async {
try {
isLoadingCurrencies.value = true;
final response = await ApiService.getMasterCurrenciesApi();
if (response != null && response.data.isNotEmpty) {
currencies.value = response.data;
} else {
currencies.clear();
}
} catch (e) {
logSafe("Error fetching currencies: $e", level: LogLevel.error);
} finally {
isLoadingCurrencies.value = false;
}
}
Future<void> fetchGlobalProjects() async {
try {
final response = await ApiService.getGlobalProjects();
if (response != null && response.isNotEmpty) {
globalProjects.value = response
.map<String>((e) => e['name']?.toString().trim() ?? '')
.where((name) => name.isNotEmpty)
.toList();
} else {
globalProjects.clear();
}
} catch (e) {
logSafe("Error fetching projects: $e", level: LogLevel.error);
globalProjects.clear();
}
}
//
// 🔹 File / Image Pickers
//
/// 📂 Pick **any type of attachment** (no extension restriction)
Future<void> pickAttachments() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.any, // No restriction on type
allowMultiple: true,
);
if (result != null && result.paths.isNotEmpty) {
attachments.addAll(result.paths.whereType<String>().map(File.new));
}
} catch (e) {
_errorSnackbar("Attachment error: $e");
}
}
/// 📸 Pick from camera and auto add timestamp
Future<void> pickFromCamera() async {
try {
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
if (pickedFile != null) {
isProcessingAttachment.value = true;
File imageFile = File(pickedFile.path);
File timestampedFile =
await TimestampImageHelper.addTimestamp(imageFile: imageFile);
attachments.add(timestampedFile);
attachments.refresh();
}
} catch (e) {
_errorSnackbar("Camera error: $e");
} finally {
isProcessingAttachment.value = false;
}
}
/// 🖼 Pick from gallery
Future<void> pickFromGallery() async {
try {
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
attachments.add(File(pickedFile.path));
attachments.refresh();
}
} catch (e) {
_errorSnackbar("Gallery error: $e");
}
}
//
// 🔹 Value Selectors
//
void selectProject(String project) => selectedProject.value = project;
void selectCategory(ExpenseCategory category) =>
selectedCategory.value = category;
void selectPayee(String payee) => selectedPayee.value = payee;
void selectCurrency(Currency currency) => selectedCurrency.value = currency;
//
// 🔹 Attachment Helpers
//
void addAttachment(File file) => attachments.add(file);
void removeAttachment(File file) => attachments.remove(file);
void removeExistingAttachment(Map<String, dynamic> file) =>
existingAttachments.remove(file);
//
// 🔹 Payload Builder (for upload)
//
Future<List<Map<String, dynamic>>> buildAttachmentPayload() async {
final existingPayload = existingAttachments
.map((e) => {
"documentId": e['documentId'],
"fileName": e['fileName'],
"contentType": e['contentType'] ?? 'application/octet-stream',
"fileSize": e['fileSize'] ?? 0,
"description": "",
"url": e['url'],
"isActive": e['isActive'] ?? true,
"base64Data": "",
})
.toList();
final newPayload = await Future.wait(
attachments.map((file) async {
final bytes = await file.readAsBytes();
return {
"fileName": file.path.split('/').last,
"base64Data": base64Encode(bytes),
"contentType":
lookupMimeType(file.path) ?? 'application/octet-stream',
"fileSize": await file.length(),
"description": "",
};
}),
);
return [...existingPayload, ...newPayload];
}
//
// 🔹 Snackbar Helper
//
void _errorSnackbar(String msg, [String title = "Error"]) {
showAppSnackbar(title: title, message: msg, type: SnackbarType.error);
}
}

View File

@ -1,10 +1,17 @@
class ApiEndpoints {
// static const String baseUrl = "https://mapi.marcoaiot.com/api";
// static const String baseUrl = "https://ofwapi.marcoaiot.com/api";
static const String baseUrl = "https://api.onfieldwork.com/api";
// static const String baseUrl = "https://api.onfieldwork.com/api";
// static const String baseUrl = "https://api.marcoaiot.com/api";
// static const String baseUrl = "https://devapi.marcoaiot.com/api";
static const String baseUrl = "https://devapi.marcoaiot.com/api";
// Finance Module API Endpoints
static const String getMasterCurrencies = "/Master/currencies/list";
static const String getMasterExpensesCategories =
"/Master/expenses-categories";
static const String getExpensePaymentRequestPayee =
"/Expense/payment-request/payee";
// Dashboard Module API Endpoints
static const String getDashboardAttendanceOverview =
"/dashboard/attendance-overview";

View File

@ -22,6 +22,10 @@ import 'package:marco/model/attendance/organization_per_project_list_model.dart'
import 'package:marco/model/dashboard/pending_expenses_model.dart';
import 'package:marco/model/dashboard/expense_type_report_model.dart';
import 'package:marco/model/dashboard/monthly_expence_model.dart';
import 'package:marco/model/finance/expense_category_model.dart';
import 'package:marco/model/finance/currency_list_model.dart';
import 'package:marco/model/finance/payment_payee_request_model.dart';
class ApiService {
static const bool enableLogs = true;
@ -291,6 +295,92 @@ class ApiService {
}
}
/// Get Master Currencies
static Future<CurrencyListResponse?> getMasterCurrenciesApi() async {
const endpoint = ApiEndpoints.getMasterCurrencies;
logSafe("Fetching Master Currencies");
try {
final response = await _getRequest(endpoint);
if (response == null) {
logSafe("Master Currencies request failed: null response",
level: LogLevel.error);
return null;
}
final jsonResponse =
_parseResponseForAllData(response, label: "Master Currencies");
if (jsonResponse != null) {
return CurrencyListResponse.fromJson(jsonResponse);
}
} catch (e, stack) {
logSafe("Exception during getMasterCurrenciesApi: $e",
level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
}
return null;
}
/// Get Master Expense Categories
static Future<ExpenseCategoryResponse?>
getMasterExpenseCategoriesApi() async {
const endpoint = ApiEndpoints.getMasterExpensesCategories;
logSafe("Fetching Master Expense Categories");
try {
final response = await _getRequest(endpoint);
if (response == null) {
logSafe("Master Expense Categories request failed: null response",
level: LogLevel.error);
return null;
}
final jsonResponse = _parseResponseForAllData(response,
label: "Master Expense Categories");
if (jsonResponse != null) {
return ExpenseCategoryResponse.fromJson(jsonResponse);
}
} catch (e, stack) {
logSafe("Exception during getMasterExpenseCategoriesApi: $e",
level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
}
return null;
}
/// Get Expense Payment Request Payee
static Future<PaymentRequestPayeeResponse?>
getExpensePaymentRequestPayeeApi() async {
const endpoint = ApiEndpoints.getExpensePaymentRequestPayee;
logSafe("Fetching Expense Payment Request Payees");
try {
final response = await _getRequest(endpoint);
if (response == null) {
logSafe("Expense Payment Request Payee request failed: null response",
level: LogLevel.error);
return null;
}
final jsonResponse = _parseResponseForAllData(response,
label: "Expense Payment Request Payee");
if (jsonResponse != null) {
return PaymentRequestPayeeResponse.fromJson(jsonResponse);
}
} catch (e, stack) {
logSafe("Exception during getExpensePaymentRequestPayeeApi: $e",
level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
}
return null;
}
/// Get Monthly Expense Report (categoryId is optional)
static Future<DashboardMonthlyExpenseResponse?>
getDashboardMonthlyExpensesApi({

View File

@ -1,4 +1,4 @@
// expense_form_widgets.dart
// form_widgets.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
@ -6,7 +6,6 @@ import 'package:url_launcher/url_launcher_string.dart';
import 'package:marco/helpers/widgets/image_viewer_dialog.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/expense/add_expense_controller.dart';
/// 🔹 Common Colors & Styles
final _hintStyle = TextStyle(fontSize: 14, color: Colors.grey[600]);
@ -161,9 +160,10 @@ class TileContainer extends StatelessWidget {
}
/// ==========================
/// Attachments Section
/// Attachments Section (Reusable)
/// ==========================
class AttachmentsSection extends StatelessWidget {
class AttachmentsSection<T extends GetxController> extends StatelessWidget {
final T controller; // 🔹 Now any controller can be passed
final RxList<File> attachments;
final RxList<Map<String, dynamic>> existingAttachments;
final ValueChanged<File> onRemoveNew;
@ -171,6 +171,7 @@ class AttachmentsSection extends StatelessWidget {
final VoidCallback onAdd;
const AttachmentsSection({
required this.controller,
required this.attachments,
required this.existingAttachments,
required this.onRemoveNew,
@ -239,8 +240,20 @@ class AttachmentsSection extends StatelessWidget {
),
)),
_buildActionTile(Icons.attach_file, onAdd),
_buildActionTile(Icons.camera_alt,
() => Get.find<AddExpenseController>().pickFromCamera()),
_buildActionTile(Icons.camera_alt, () {
// 🔹 Dynamically call pickFromCamera if it exists
final fn = controller as dynamic;
if (fn.pickFromCamera != null) {
fn.pickFromCamera();
} else {
showAppSnackbar(
title: 'Error',
message:
'This controller does not support camera attachments.',
type: SnackbarType.error,
);
}
}),
],
),
],
@ -402,7 +415,6 @@ class _AttachmentTile extends StatelessWidget {
);
}
/// map extensions to icons/colors
static (IconData, Color) _fileIcon(String ext) {
switch (ext) {
case 'pdf':

View File

@ -457,6 +457,7 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
attachments: controller.attachments,
existingAttachments: controller.existingAttachments,
onRemoveNew: controller.removeAttachment,
controller: controller,
onRemoveExisting: (item) async {
await showDialog(
context: context,

View File

@ -0,0 +1,396 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/finance/add_payment_request_controller.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
import 'package:marco/helpers/utils/validators.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/widgets/expense/expense_form_widgets.dart';
import 'package:marco/helpers/widgets/my_confirmation_dialog.dart';
/// Show Payment Request Bottom Sheet
Future<T?> showPaymentRequestBottomSheet<T>({bool isEdit = false}) {
return Get.bottomSheet<T>(
_PaymentRequestBottomSheet(isEdit: isEdit),
isScrollControlled: true,
);
}
class _PaymentRequestBottomSheet extends StatefulWidget {
final bool isEdit;
const _PaymentRequestBottomSheet({this.isEdit = false});
@override
State<_PaymentRequestBottomSheet> createState() =>
_PaymentRequestBottomSheetState();
}
class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet>
with UIMixin {
final PaymentRequestController controller =
Get.put(PaymentRequestController());
final _formKey = GlobalKey<FormState>();
final GlobalKey _projectDropdownKey = GlobalKey();
final GlobalKey _categoryDropdownKey = GlobalKey();
final GlobalKey _payeeDropdownKey = GlobalKey();
final GlobalKey _currencyDropdownKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Obx(
() => Form(
key: _formKey,
child: BaseBottomSheet(
title:
widget.isEdit ? "Edit Payment Request" : "Create Payment Request",
isSubmitting: false,
onCancel: Get.back,
onSubmit: () {
if (_formKey.currentState!.validate() && _validateSelections()) {
// Call your submit API here
showAppSnackbar(
title: "Success",
message: "Payment request submitted!",
type: SnackbarType.success,
);
Get.back();
}
},
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDropdownField<String>(
icon: Icons.work_outline,
title: " Select Project",
requiredField: true,
value: controller.selectedProject.value.isEmpty
? "Select Project"
: controller.selectedProject.value,
onTap: () => _showOptionList<String>(
controller.globalProjects.toList(),
(p) => p,
controller.selectProject,
_projectDropdownKey,
),
dropdownKey: _projectDropdownKey,
),
_gap(),
_buildDropdownField(
icon: Icons.category_outlined,
title: "Expense Category",
requiredField: true,
value: controller.selectedCategory.value?.name ??
"Select Category",
onTap: () => _showOptionList(
controller.categories.toList(),
(c) => c.name,
controller.selectCategory,
_categoryDropdownKey),
dropdownKey: _categoryDropdownKey,
),
_gap(),
_buildTextField(
icon: Icons.title_outlined,
title: "Title",
controller: TextEditingController(),
hint: "Enter title",
validator: Validators.requiredField,
),
_gap(),
// Is Advance Payment Radio Buttons with Icon and Primary Color
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.attach_money_outlined, size: 20),
SizedBox(width: 6),
Text(
"Is Advance Payment",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
MySpacing.height(6),
Obx(() => Row(
children: [
Expanded(
child: RadioListTile<bool>(
contentPadding: EdgeInsets.zero,
title: Text("Yes"),
value: true,
groupValue: controller.isAdvancePayment.value,
activeColor: contentTheme.primary,
onChanged: (val) {
if (val != null)
controller.isAdvancePayment.value = val;
},
),
),
Expanded(
child: RadioListTile<bool>(
contentPadding: EdgeInsets.zero,
title: Text("No"),
value: false,
groupValue: controller.isAdvancePayment.value,
activeColor: contentTheme.primary,
onChanged: (val) {
if (val != null)
controller.isAdvancePayment.value = val;
},
),
),
],
)),
_gap(),
],
),
_buildTextField(
icon: Icons.calendar_today,
title: "Due To Date",
controller: TextEditingController(),
hint: "DD-MM-YYYY",
validator: Validators.requiredField,
),
_gap(),
_buildTextField(
icon: Icons.currency_rupee,
title: "Amount",
controller: TextEditingController(),
hint: "Enter Amount",
keyboardType: TextInputType.number,
validator: (v) =>
(v != null && v.isNotEmpty && double.tryParse(v) != null)
? null
: "Enter valid amount",
),
_gap(),
_buildDropdownField<String>(
icon: Icons.person_outline,
title: "Payee",
requiredField: true,
value: controller.selectedPayee.value.isEmpty
? "Select Payee"
: controller.selectedPayee.value,
onTap: () => _showOptionList(controller.payees.toList(),
(p) => p, controller.selectPayee, _payeeDropdownKey),
dropdownKey: _payeeDropdownKey,
),
_gap(),
_buildDropdownField(
icon: Icons.monetization_on_outlined,
title: "Currency",
requiredField: true,
value: controller.selectedCurrency.value?.currencyName ??
"Select Currency",
onTap: () => _showOptionList(
controller.currencies.toList(),
(c) => c.currencyName, // <-- changed here
controller.selectCurrency,
_currencyDropdownKey),
dropdownKey: _currencyDropdownKey,
),
_gap(),
_buildTextField(
icon: Icons.description_outlined,
title: "Description",
controller: TextEditingController(),
hint: "Enter description",
maxLines: 3,
validator: Validators.requiredField,
),
_gap(),
_buildAttachmentsSection(),
],
),
),
),
),
);
}
Widget _gap([double h = 16]) => MySpacing.height(h);
Widget _buildDropdownField<T>({
required IconData icon,
required String title,
required bool requiredField,
required String value,
required VoidCallback onTap,
required GlobalKey dropdownKey,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionTitle(icon: icon, title: title, requiredField: requiredField),
MySpacing.height(6),
DropdownTile(key: dropdownKey, title: value, onTap: onTap),
],
);
}
Widget _buildTextField({
required IconData icon,
required String title,
required TextEditingController controller,
String? hint,
TextInputType? keyboardType,
FormFieldValidator<String>? validator,
int maxLines = 1,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionTitle(
icon: icon, title: title, requiredField: validator != null),
MySpacing.height(6),
CustomTextField(
controller: controller,
hint: hint ?? "",
keyboardType: keyboardType ?? TextInputType.text,
validator: validator,
maxLines: maxLines,
),
],
);
}
Widget _buildAttachmentsSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SectionTitle(
icon: Icons.attach_file,
title: "Attachments",
requiredField: true,
),
MySpacing.height(10),
Obx(() {
if (controller.isProcessingAttachment.value) {
return Center(
child: Column(
children: [
CircularProgressIndicator(
color: contentTheme.primary,
),
const SizedBox(height: 8),
Text(
"Processing image, please wait...",
style: TextStyle(
fontSize: 14,
color: contentTheme.primary,
),
),
],
),
);
}
return AttachmentsSection(
attachments: controller.attachments,
existingAttachments: controller.existingAttachments,
onRemoveNew: controller.removeAttachment,
controller: controller,
onRemoveExisting: (item) async {
await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => ConfirmDialog(
title: "Remove Attachment",
message: "Are you sure you want to remove this attachment?",
confirmText: "Remove",
icon: Icons.delete,
confirmColor: Colors.redAccent,
onConfirm: () async {
final index = controller.existingAttachments.indexOf(item);
if (index != -1) {
controller.existingAttachments[index]['isActive'] = false;
controller.existingAttachments.refresh();
}
showAppSnackbar(
title: 'Removed',
message: 'Attachment has been removed.',
type: SnackbarType.success,
);
Navigator.pop(context);
},
),
);
},
onAdd: controller.pickAttachments,
);
}),
],
);
}
/// Generic option list for dropdowns
Future<void> _showOptionList<T>(List<T> options, String Function(T) getLabel,
ValueChanged<T> onSelected, GlobalKey key) async {
if (options.isEmpty) {
_showError("No options available");
return;
}
final RenderBox button =
key.currentContext!.findRenderObject() as RenderBox;
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
final position = button.localToGlobal(Offset.zero, ancestor: overlay);
final selected = await showMenu<T>(
context: context,
position: RelativeRect.fromLTRB(
position.dx,
position.dy + button.size.height,
overlay.size.width - position.dx - button.size.width,
0,
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
items: options
.map((opt) => PopupMenuItem<T>(
value: opt,
child: Text(getLabel(opt)),
))
.toList(),
);
if (selected != null) onSelected(selected);
}
bool _validateSelections() {
if (controller.selectedProject.value.isEmpty) {
_showError("Please select a project");
return false;
}
if (controller.selectedCategory.value == null) {
_showError("Please select a category");
return false;
}
if (controller.selectedPayee.value.isEmpty) {
_showError("Please select a payee");
return false;
}
if (controller.selectedCurrency.value == null) {
_showError("Please select currency");
return false;
}
return true;
}
void _showError(String msg) {
showAppSnackbar(
title: "Error",
message: msg,
type: SnackbarType.error,
);
}
}

View File

@ -0,0 +1,77 @@
class CurrencyListResponse {
final bool success;
final String message;
final List<Currency> data;
final dynamic errors;
final int statusCode;
final DateTime timestamp;
CurrencyListResponse({
required this.success,
required this.message,
required this.data,
this.errors,
required this.statusCode,
required this.timestamp,
});
factory CurrencyListResponse.fromJson(Map<String, dynamic> json) {
return CurrencyListResponse(
success: json['success'] ?? false,
message: json['message'] ?? '',
data: (json['data'] as List<dynamic>? ?? [])
.map((e) => Currency.fromJson(e))
.toList(),
errors: json['errors'],
statusCode: json['statusCode'] ?? 0,
timestamp: DateTime.parse(json['timestamp']),
);
}
Map<String, dynamic> toJson() {
return {
'success': success,
'message': message,
'data': data.map((e) => e.toJson()).toList(),
'errors': errors,
'statusCode': statusCode,
'timestamp': timestamp.toIso8601String(),
};
}
}
class Currency {
final String id;
final String currencyCode;
final String currencyName;
final String symbol;
final bool isActive;
Currency({
required this.id,
required this.currencyCode,
required this.currencyName,
required this.symbol,
required this.isActive,
});
factory Currency.fromJson(Map<String, dynamic> json) {
return Currency(
id: json['id'] ?? '',
currencyCode: json['currencyCode'] ?? '',
currencyName: json['currencyName'] ?? '',
symbol: json['symbol'] ?? '',
isActive: json['isActive'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'currencyCode': currencyCode,
'currencyName': currencyName,
'symbol': symbol,
'isActive': isActive,
};
}
}

View File

@ -0,0 +1,77 @@
class ExpenseCategoryResponse {
final bool success;
final String message;
final List<ExpenseCategory> data;
final dynamic errors;
final int statusCode;
final DateTime timestamp;
ExpenseCategoryResponse({
required this.success,
required this.message,
required this.data,
this.errors,
required this.statusCode,
required this.timestamp,
});
factory ExpenseCategoryResponse.fromJson(Map<String, dynamic> json) {
return ExpenseCategoryResponse(
success: json['success'] ?? false,
message: json['message'] ?? '',
data: (json['data'] as List<dynamic>? ?? [])
.map((e) => ExpenseCategory.fromJson(e))
.toList(),
errors: json['errors'],
statusCode: json['statusCode'] ?? 0,
timestamp: DateTime.parse(json['timestamp']),
);
}
Map<String, dynamic> toJson() {
return {
'success': success,
'message': message,
'data': data.map((e) => e.toJson()).toList(),
'errors': errors,
'statusCode': statusCode,
'timestamp': timestamp.toIso8601String(),
};
}
}
class ExpenseCategory {
final String id;
final String name;
final bool noOfPersonsRequired;
final bool isAttachmentRequried;
final String description;
ExpenseCategory({
required this.id,
required this.name,
required this.noOfPersonsRequired,
required this.isAttachmentRequried,
required this.description,
});
factory ExpenseCategory.fromJson(Map<String, dynamic> json) {
return ExpenseCategory(
id: json['id'] ?? '',
name: json['name'] ?? '',
noOfPersonsRequired: json['noOfPersonsRequired'] ?? false,
isAttachmentRequried: json['isAttachmentRequried'] ?? false,
description: json['description'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'noOfPersonsRequired': noOfPersonsRequired,
'isAttachmentRequried': isAttachmentRequried,
'description': description,
};
}
}

View File

@ -0,0 +1,39 @@
class PaymentRequestPayeeResponse {
final bool success;
final String message;
final List<String> data;
final dynamic errors;
final int statusCode;
final DateTime timestamp;
PaymentRequestPayeeResponse({
required this.success,
required this.message,
required this.data,
this.errors,
required this.statusCode,
required this.timestamp,
});
factory PaymentRequestPayeeResponse.fromJson(Map<String, dynamic> json) {
return PaymentRequestPayeeResponse(
success: json['success'] ?? false,
message: json['message'] ?? '',
data: List<String>.from(json['data'] ?? []),
errors: json['errors'],
statusCode: json['statusCode'] ?? 0,
timestamp: DateTime.parse(json['timestamp']),
);
}
Map<String, dynamic> toJson() {
return {
'success': success,
'message': message,
'data': data,
'errors': errors,
'statusCode': statusCode,
'timestamp': timestamp.toIso8601String(),
};
}
}

View File

@ -5,6 +5,7 @@ import 'package:marco/controller/project_controller.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/model/finance/add_payment_request_bottom_sheet.dart';
class FinanceScreen extends StatefulWidget {
const FinanceScreen({super.key});