diff --git a/lib/controller/finance/advance_payment_controller.dart b/lib/controller/finance/advance_payment_controller.dart new file mode 100644 index 0000000..1e159fc --- /dev/null +++ b/lib/controller/finance/advance_payment_controller.dart @@ -0,0 +1,144 @@ +import 'dart:async'; +import 'package:get/get.dart'; +import 'package:marco/model/finance/advance_payment_model.dart'; +import 'package:marco/model/finance/get_employee_model.dart'; +import 'package:marco/helpers/services/app_logger.dart'; +import 'package:marco/helpers/services/api_service.dart'; + +class AdvancePaymentController extends GetxController { + /// Advance payments list + var payments = [].obs; + var isLoading = false.obs; + + /// Employees for dropdown search + var employees = [].obs; + var allEmployees = []; // cache of last API response + var employeesLoading = false.obs; + var searchQuery = ''.obs; + var selectedEmployee = Rxn(); + + /// Prevents unwanted API calls while programmatically updating search + var _suppressSearch = false.obs; + + Timer? _debounceTimer; + + @override + void onInit() { + super.onInit(); + + ever(searchQuery, (q) { + if (_suppressSearch.value) return; // Skip while selecting employee + + // 🔹 When user types new text, clear previous employee + payments instantly + if (selectedEmployee.value != null) { + selectedEmployee.value = null; + payments.clear(); + } + + // 🔹 Show fresh dropdown results for new query + _debounceTimer?.cancel(); + _debounceTimer = Timer(const Duration(milliseconds: 400), () { + if (q.isNotEmpty) { + fetchEmployees(q); // repopulate dropdown + } else { + employees.clear(); // hide dropdown when search cleared + } + }); + }); + } + + @override + void onClose() { + _debounceTimer?.cancel(); + super.onClose(); + } + + /// Fetch employees by query + Future fetchEmployees(String q) async { + if (q.isEmpty) { + employees.clear(); + return; + } + + if (employeesLoading.value) return; + + try { + employeesLoading.value = true; + + final list = await ApiService.getEmployees(query: q); + final parsed = Employee.listFromJson(list); + logSafe("✅ Employees fetched from API: ${parsed.map((e) => e.name).toList()}"); + + + // Save full result and filter locally + allEmployees = parsed; + _filterEmployees(q); + } catch (e, s) { + logSafe("❌ fetchEmployees error: $e\n$s", level: LogLevel.error); + employees.clear(); + } finally { + employeesLoading.value = false; + } + } + + /// Local filter to update list based on search text + void _filterEmployees(String query) { + final q = query.toLowerCase(); + employees + ..clear() + ..addAll(allEmployees.where((e) { + return e.name.toLowerCase().contains(q) || + e.email.toLowerCase().contains(q); + })); + } + + /// When user selects employee + void selectEmployee(Employee emp) { + _suppressSearch.value = true; + + selectedEmployee.value = emp; + employees.clear(); // hide dropdown + searchQuery.value = emp.name; + + fetchAdvancePayments(emp.id); + + // Re-enable search after a short delay + Future.delayed(const Duration(milliseconds: 300), () { + _suppressSearch.value = false; + }); + } + + /// Fetch advance payments for the selected employee + Future fetchAdvancePayments(String employeeId) async { + if (employeeId.isEmpty) { + payments.clear(); + return; + } + + try { + isLoading.value = true; + final list = await ApiService.getAdvancePayments(employeeId); + payments.assignAll(list); + } catch (e, s) { + logSafe("❌ fetchAdvancePayments error: $e\n$s", level: LogLevel.error); + payments.clear(); + } finally { + isLoading.value = false; + } + } + + /// Clear employee selection + void clearSelection() { + selectedEmployee.value = null; + payments.clear(); + employees.clear(); + searchQuery.value = ''; + } + + void resetSelectionOnNewSearch() { + if (selectedEmployee.value != null) { + selectedEmployee.value = null; + payments.clear(); + } + } +} diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index b9c4d9c..fa46ee4 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,9 +1,9 @@ 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"; // Dashboard Module API Endpoints static const String getDashboardAttendanceOverview = @@ -103,4 +103,8 @@ class ApiEndpoints { static const String getAssignedOrganizations = "/project/get/assigned/organization"; static const String getAssignedServices = "/Project/get/assigned/services"; + + static const String getAdvancePayments = '/Expense/get/transactions'; + + static const String getEmployees = "/Employee/basic"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 13aecfa..06015c7 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -22,6 +22,7 @@ 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/advance_payment_model.dart'; class ApiService { static const bool enableLogs = true; @@ -173,6 +174,29 @@ class ApiService { } } + static Future get(String endpoint, + {Map? queryParams}) async { + final response = await _getRequest(endpoint, queryParams: queryParams); + + if (response == null) return null; + + try { + final body = jsonDecode(response.body); + if (response.statusCode == 200) { + return body; + } else { + return { + "success": false, + "statusCode": response.statusCode, + "body": body, + }; + } + } catch (e) { + logSafe("❌ ApiService.get JSON parse error: $e", level: LogLevel.error); + return null; + } + } + static Future _postRequest( String endpoint, dynamic body, { @@ -1297,6 +1321,103 @@ class ApiService { } } + static Future> getAdvancePayments( + String employeeId) async { + try { + final endpoint = "${ApiEndpoints.getAdvancePayments}/$employeeId"; + + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("❌ getAdvancePayments: Null response"); + return []; + } + + if (response.statusCode == 200) { + // 🟢 Added log to inspect raw JSON + logSafe("🔍 AdvancePayment raw response: ${response.body}"); + + final Map body = jsonDecode(response.body); + final data = body['data'] ?? body; + return AdvancePayment.listFromJson(data); + } else { + logSafe("⚠ getAdvancePayments failed → ${response.statusCode}"); + return []; + } + } catch (e, s) { + logSafe("❌ ApiService.getAdvancePayments error: $e\n$s", + level: LogLevel.error); + return []; + } + } + + /// Fetch employees with optional query. Returns raw list (List) + static Future> getEmployees({String query = ''}) async { + try { + // endpoint relative to ApiEndpoints.baseUrl; _getRequest builds full url + var endpoint = "/Employee/basic"; + Map? queryParams; + if (query.isNotEmpty) { + // server may expect a query param name other than 'q'. Adjust if needed. + queryParams = {'q': query}; + } + + final resp = await _getRequest(endpoint, queryParams: queryParams); + if (resp == null) return []; + + // parse response + try { + final body = jsonDecode(resp.body); + if (body is Map && body.containsKey('data')) { + final data = body['data']; + if (data is List) return data; + return []; + } else if (body is List) { + return body; + } else { + return []; + } + } catch (e, s) { + logSafe("❌ ApiService.getEmployees: parse error $e\n$s", + level: LogLevel.error); + return []; + } + } catch (e, s) { + logSafe("❌ ApiService.getEmployees error: $e\n$s", level: LogLevel.error); + return []; + } + } + + /// Fetch advance payments list from backend and parse into model objects + static Future> getAdvancePaymentsRaw(String employeeId) async { + try { + final endpoint = "${ApiEndpoints.getAdvancePayments}/$employeeId"; + final resp = await _getRequest(endpoint); + if (resp == null) return []; + + try { + final body = jsonDecode(resp.body); + if (body is Map && body.containsKey('data')) { + final data = body['data']; + if (data is List) return data; + return []; + } else if (body is List) { + return body; + } else { + return []; + } + } catch (e, s) { + logSafe("❌ ApiService.getAdvancePaymentsRaw parse error: $e\n$s", + level: LogLevel.error); + return []; + } + } catch (e, s) { + logSafe("❌ ApiService.getAdvancePaymentsRaw error: $e\n$s", + level: LogLevel.error); + return []; + } + } + /// Fetch Master Payment Modes static Future?> getMasterPaymentModes() async { const endpoint = ApiEndpoints.getMasterPaymentModes; diff --git a/lib/model/finance/advance_payment_model.dart b/lib/model/finance/advance_payment_model.dart new file mode 100644 index 0000000..0626c2d --- /dev/null +++ b/lib/model/finance/advance_payment_model.dart @@ -0,0 +1,63 @@ +class AdvancePayment { + final String id; + final String title; + final String name; + final double amount; + final double balance; + final String date; + + const AdvancePayment({ + required this.id, + required this.title, + required this.name, + required this.amount, + required this.balance, + required this.date, + }); + + factory AdvancePayment.fromJson(Map json) { + double parseDouble(dynamic value) { + if (value == null) return 0.0; + if (value is num) return value.toDouble(); + if (value is String) return double.tryParse(value) ?? 0.0; + return 0.0; + } + + String extractProjectName(dynamic project) { + if (project is Map && project.containsKey('name')) { + return project['name']?.toString() ?? ''; + } else if (project is String) { + return project; + } + return ''; + } + + return AdvancePayment( + id: json['id']?.toString() ?? '', + // 👇 Fallback for APIs using "description" instead of "title" + title: json['title']?.toString() ?? json['description']?.toString() ?? '', + name: extractProjectName(json['project']), + amount: parseDouble(json['amount']), + balance: parseDouble(json['currentBalance']), + date: json['paidAt']?.toString() ?? json['createdAt']?.toString() ?? '', + ); + } + + static List listFromJson(dynamic data) { + if (data is List) { + return data + .map((e) => e is Map + ? AdvancePayment.fromJson(Map.from(e)) + : const AdvancePayment( + id: '', + title: '', + name: '', + amount: 0, + balance: 0, + date: '', + )) + .toList(); + } + return []; + } +} diff --git a/lib/model/finance/get_employee_model.dart b/lib/model/finance/get_employee_model.dart new file mode 100644 index 0000000..66d2448 --- /dev/null +++ b/lib/model/finance/get_employee_model.dart @@ -0,0 +1,102 @@ +class Employee { + final String id; + final String name; + final String email; + + Employee({ + required this.id, + required this.name, + required this.email, + }); + + /// Computed getters for first and last names + String get firstName { + final parts = name.trim().split(RegExp(r'\s+')); + return parts.isNotEmpty ? parts.first : ''; + } + + String get lastName { + final parts = name.trim().split(RegExp(r'\s+')); + if (parts.length > 1) { + return parts.sublist(1).join(' '); + } + return ''; + } + + factory Employee.fromJson(Map json) { + // Try many possible id fields + final idVal = (json['id'] ?? + json['Id'] ?? + json['employeeId'] ?? + json['empId'] ?? + json['employee_id']) + ?.toString(); + + // Try many possible first/last name fields + final first = (json['firstName'] ?? + json['first_name'] ?? + json['firstname'] ?? + json['fname'] ?? + '') + .toString() + .trim(); + final last = (json['lastName'] ?? + json['last_name'] ?? + json['lastname'] ?? + json['lname'] ?? + '') + .toString() + .trim(); + + // Name may come as a single field in multiple variants + String nameVal = (json['name'] ?? + json['Name'] ?? + json['fullName'] ?? + json['full_name'] ?? + json['employeeName'] ?? + json['employee_name'] ?? + json['empName'] ?? + json['employee'] ?? + '') + .toString() + .trim(); + + // If separate first/last found and name empty, combine them + if (nameVal.isEmpty && (first.isNotEmpty || last.isNotEmpty)) { + nameVal = ('$first ${last}').trim(); + } + + // If name still empty, fallback to email or id to avoid blank name + if (nameVal.isEmpty && (json['email'] != null)) { + nameVal = json['email'].toString().split('@').first; + } + if (nameVal.isEmpty) { + nameVal = idVal ?? ''; + } + + final emailVal = (json['email'] ?? + json['emailAddress'] ?? + json['email_address'] ?? + json['employeeEmail'] ?? + json['employee_email'] ?? + '') + .toString(); + + return Employee( + id: idVal ?? '', + name: nameVal, + email: emailVal, + ); + } + + static List listFromJson(dynamic data) { + if (data is List) { + return data.map((e) { + if (e is Map) return Employee.fromJson(e); + return Employee.fromJson(Map.from(e)); + }).toList(); + } + // In case API returns { data: [...] }, that should already be unwrapped by ApiService + return []; + } +} diff --git a/lib/routes.dart b/lib/routes.dart index d63efba..3e6ede6 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/helpers/services/auth_service.dart'; @@ -19,6 +18,8 @@ import 'package:marco/view/directory/directory_main_screen.dart'; import 'package:marco/view/expense/expense_screen.dart'; import 'package:marco/view/document/user_document_screen.dart'; import 'package:marco/view/finance/finance_screen.dart'; +import 'package:marco/view/finance/advance_payment_screen.dart'; + class AuthMiddleware extends GetMiddleware { @override @@ -54,23 +55,23 @@ getPageRoute() { name: '/dashboard/employees', page: () => EmployeesScreen(), middlewares: [AuthMiddleware()]), - + GetPage( name: '/dashboard/directory-main-page', page: () => DirectoryMainScreen(), middlewares: [AuthMiddleware()]), - + // Finance GetPage( name: '/dashboard/finance', page: () => FinanceScreen(), middlewares: [AuthMiddleware()]), - // Expense + // Expense GetPage( name: '/dashboard/expense-main-page', page: () => ExpenseMainScreen(), middlewares: [AuthMiddleware()]), - // Documents + // Documents GetPage( name: '/dashboard/document-main-page', page: () => UserDocumentsPage(), @@ -99,6 +100,19 @@ getPageRoute() { name: '/error/404', page: () => Error404Screen(), middlewares: [AuthMiddleware()]), + + // Finance + GetPage( + name: '/dashboard/finance', + page: () => FinanceScreen(), + middlewares: [AuthMiddleware()], + ), + // Advance Payment + GetPage( + name: '/dashboard/finance/advance-payment', + page: () => AdvancePaymentScreen(), + middlewares: [AuthMiddleware()], + ), ]; return routes .map((e) => GetPage( diff --git a/lib/view/faq/faq_screen.dart b/lib/view/faq/faq_screen.dart index 1f33a67..db364ed 100644 --- a/lib/view/faq/faq_screen.dart +++ b/lib/view/faq/faq_screen.dart @@ -115,7 +115,7 @@ class _FAQScreenState extends State with UIMixin { color: contentTheme.primary.withOpacity(0.1), shape: BoxShape.circle, ), - child: Icon(LucideIcons.badge_help, + child: Icon(LucideIcons.badge_alert, color: contentTheme.primary, size: 24), ), const SizedBox(width: 16), diff --git a/lib/view/finance/advance_payment_screen.dart b/lib/view/finance/advance_payment_screen.dart new file mode 100644 index 0000000..f4e22ba --- /dev/null +++ b/lib/view/finance/advance_payment_screen.dart @@ -0,0 +1,471 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/finance/advance_payment_controller.dart'; +import 'package:marco/controller/project_controller.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:flutter/services.dart'; +import 'package:intl/intl.dart'; + +class AdvancePaymentScreen extends StatefulWidget { + const AdvancePaymentScreen({super.key}); + + @override + State createState() => _AdvancePaymentScreenState(); +} + +class _AdvancePaymentScreenState extends State + with UIMixin { + late final AdvancePaymentController controller; + late final TextEditingController _searchCtrl; + final FocusNode _searchFocus = FocusNode(); + + @override + void initState() { + super.initState(); + controller = Get.put(AdvancePaymentController()); + _searchCtrl = TextEditingController(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + final employeeId = Get.arguments?['employeeId'] ?? ''; + if (employeeId.isNotEmpty) { + controller.fetchAdvancePayments(employeeId); + } + }); + + controller.searchQuery.listen((q) { + if (_searchCtrl.text != q) _searchCtrl.text = q; + }); + } + + @override + void dispose() { + _searchCtrl.dispose(); + _searchFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final projectController = Get.find(); + + return Scaffold( + backgroundColor: const Color( + 0xFFF5F5F5), // ✅ light grey background (Expense screen style) + appBar: _buildAppBar(projectController), + body: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: RefreshIndicator( + onRefresh: () async { + final emp = controller.selectedEmployee.value; + if (emp != null) { + await controller.fetchAdvancePayments(emp.id.toString()); + } + }, + color: Colors.white, // spinner color + backgroundColor: Colors.blue, // circle background color + strokeWidth: 2.5, + displacement: 60, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Container( + color: + const Color(0xFFF5F5F5), // ✅ match background inside scroll + child: Column( + children: [ + _buildSearchBar(), + _buildEmployeeDropdown(context), + _buildTopBalance(), + _buildPaymentList(), + ], + ), + ), + ), + ), + ), + ); + } + + // ---------------- AppBar ---------------- + PreferredSizeWidget _buildAppBar(ProjectController projectController) { + return AppBar( + backgroundColor: Colors.grey[100], + elevation: 0.5, + automaticallyImplyLeading: false, + titleSpacing: 0, + title: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios_new, + color: Colors.black, size: 20), + onPressed: () => Get.offNamed('/dashboard/finance'), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleLarge("Advance Payment", + fontWeight: 700, color: Colors.black), + MySpacing.height(2), + GetBuilder( + builder: (_) { + final projectName = + projectController.selectedProject?.name ?? + 'Select Project'; + return Row( + children: [ + const Icon(Icons.work_outline, + size: 14, color: Colors.grey), + MySpacing.width(4), + Expanded( + child: MyText.bodySmall( + projectName, + overflow: TextOverflow.ellipsis, + fontWeight: 600, + color: Colors.grey[700], + ), + ), + ], + ); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } + + // ---------------- Search ---------------- + Widget _buildSearchBar() { + return Container( + color: Colors.grey[100], + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + Expanded( + child: SizedBox( + height: 38, + child: TextField( + controller: _searchCtrl, + focusNode: _searchFocus, + onChanged: (v) => controller.searchQuery.value = v.trim(), + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 0), + prefixIcon: + const Icon(Icons.search, size: 20, color: Colors.grey), + hintText: 'Search Employee...', + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: + BorderSide(color: Colors.grey.shade300, width: 1), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: + BorderSide(color: Colors.grey.shade300, width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: + BorderSide(color: contentTheme.primary, width: 1.5), + ), + ), + ), + ), + ), + const SizedBox(width: 4), + IconButton( + icon: const Icon(Icons.tune, color: Colors.black), + onPressed: () {}, + ), + ], + ), + ); + } + + // ---------------- Employee Dropdown ---------------- + Widget _buildEmployeeDropdown(BuildContext context) { + return Obx(() { + if (controller.employees.isEmpty || + controller.selectedEmployee.value != null) { + return const SizedBox.shrink(); + } + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 6, + offset: const Offset(0, 3)) + ], + ), + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.4, + ), + child: ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 6), + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + itemCount: controller.employees.length, + separatorBuilder: (_, __) => + Divider(height: 1, color: Colors.grey.shade200), + itemBuilder: (_, i) => _buildEmployeeItem(controller.employees[i]), + ), + ); + }); + } + + Widget _buildEmployeeItem(dynamic e) { + return InkWell( + onTap: () { + controller.selectEmployee(e); + _searchCtrl.text = e.name; + controller.searchQuery.value = e.name; + FocusScope.of(context).unfocus(); + SystemChannels.textInput.invokeMethod('TextInput.hide'); + controller.employees.clear(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + CircleAvatar( + radius: 18, + backgroundColor: _avatarColorFor(e.name), + child: Text( + _initials(e.firstName, e.lastName), + style: const TextStyle(color: Colors.white), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(e.name, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Colors.black87)), + if (e.email.isNotEmpty) + Text(e.email, + style: TextStyle( + fontSize: 13, color: Colors.grey.shade600)), + ], + ), + ), + ], + ), + ), + ); + } + + // ---------------- Current Balance ---------------- + Widget _buildTopBalance() { + return Obx(() { + if (controller.payments.isEmpty) return const SizedBox.shrink(); + final bal = controller.payments.first.balance.truncate(); + + return Container( + width: double.infinity, + color: Colors.grey[100], + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + const Text( + "Current Balance : ", + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.green, + fontSize: 22, + ), + ), + Text( + "₹${_formatAmount(bal)}", + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.green, + fontSize: 22, + ), + ), + ], + ), + ); + }); + } + + // ---------------- Payments List ---------------- + Widget _buildPaymentList() { + return Obx(() { + if (controller.isLoading.value) { + return const Padding( + padding: EdgeInsets.only(top: 100), + child: Center(child: CircularProgressIndicator(color: Colors.blue)), + ); + } + + // ✅ No employee selected yet + if (controller.selectedEmployee.value == null) { + return const Padding( + padding: EdgeInsets.only(top: 100), + child: Center(child: Text("Please select an Employee")), + ); + } + + // ✅ Employee selected but no payments found + if (controller.payments.isEmpty) { + return const Padding( + padding: EdgeInsets.only(top: 100), + child: Center( + child: Text("No advance payment transactions found."), + ), + ); + } + + // ✅ Payments available + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 6), + itemCount: controller.payments.length, + itemBuilder: (context, index) => + _buildPaymentItem(controller.payments[index]), + ); + }); + } + + // ---------------- Payment Item ---------------- + Widget _buildPaymentItem(dynamic item) { + final dateStr = (item.date ?? '').toString(); + DateTime? parsedDate; + try { + parsedDate = DateTime.parse(dateStr); + } catch (_) {} + + final formattedDate = parsedDate != null + ? DateFormat('dd MMM yyyy').format(parsedDate) + : (dateStr.isNotEmpty ? dateStr : '—'); + + final formattedTime = + parsedDate != null ? DateFormat('hh:mm a').format(parsedDate) : ''; + + final project = item.name ?? ''; + final desc = item.title ?? ''; + final amount = (item.amount ?? 0).toDouble(); + final isCredit = amount >= 0; + final accentColor = isCredit ? Colors.green.shade700 : Colors.red.shade700; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: Colors.grey[100], + border: Border( + bottom: BorderSide(color: Color(0xFFE0E0E0), width: 0.9), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + formattedDate, + style: + TextStyle(fontSize: 12, color: Colors.grey.shade600), + ), + if (formattedTime.isNotEmpty) ...[ + const SizedBox(width: 6), + Text( + formattedTime, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade500, + fontStyle: FontStyle.italic), + ), + ] + ], + ), + const SizedBox(height: 4), + Text( + project.isNotEmpty ? project : 'No Project', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + const SizedBox(height: 4), + Text( + desc.isNotEmpty ? desc : 'No Details', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade700, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + Text( + "${isCredit ? '+' : '-'} ₹${_formatAmount(amount)}", + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: accentColor, + ), + ), + ], + ), + ); + } + + // ---------------- Utilities ---------------- + String _initials(String? firstName, [String? lastName]) { + if ((firstName?.isEmpty ?? true) && (lastName?.isEmpty ?? true)) return '?'; + return ((firstName?.isNotEmpty == true ? firstName![0] : '') + + (lastName?.isNotEmpty == true ? lastName![0] : '')) + .toUpperCase(); + } + + String _formatAmount(num amount) { + final format = NumberFormat('#,##,###.##', 'en_IN'); + return format.format(amount); + } + + static Color _avatarColorFor(String name) { + final colors = [ + Colors.green, + Colors.indigo, + Colors.orange, + Colors.blueGrey, + Colors.deepPurple, + Colors.teal, + Colors.amber, + ]; + final hash = name.codeUnits.fold(0, (p, e) => p + e); + return colors[hash % colors.length]; + } +} diff --git a/lib/view/finance/finance_screen.dart b/lib/view/finance/finance_screen.dart index 49d3e8a..64f5bb1 100644 --- a/lib/view/finance/finance_screen.dart +++ b/lib/view/finance/finance_screen.dart @@ -262,7 +262,7 @@ class _FinanceScreenState extends State "Advance Payment", "Manage advance payments", contentTheme.warning, - "/dashboard/advance-payment", + "/dashboard/finance/advance-payment", ), ]; diff --git a/lib/view/layouts/user_profile_right_bar.dart b/lib/view/layouts/user_profile_right_bar.dart index b55a2ef..d0d7d77 100644 --- a/lib/view/layouts/user_profile_right_bar.dart +++ b/lib/view/layouts/user_profile_right_bar.dart @@ -215,7 +215,7 @@ class _UserProfileBarState extends State ), SizedBox(height: spacingHeight), _menuItemRow( - icon: LucideIcons.badge_help, + icon: LucideIcons.badge_alert, label: 'FAQ', onTap: () { Get.to(() => FAQScreen());