diff --git a/admin_panel/admin_panel/page/cashout_requests/__init__.py b/admin_panel/admin_panel/page/cashout_requests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/admin_panel/admin_panel/page/cashout_requests/cashout_requests.js b/admin_panel/admin_panel/page/cashout_requests/cashout_requests.js new file mode 100644 index 0000000..d29947b --- /dev/null +++ b/admin_panel/admin_panel/page/cashout_requests/cashout_requests.js @@ -0,0 +1,1048 @@ +frappe.pages['cashout-requests'].on_page_load = function(wrapper) { + if (!frappe.user_roles.includes('Accounts Manager')) { + var page = frappe.ui.make_app_page({ + parent: wrapper, + title: 'Cashout Requests', + single_column: true + }); + + page.main.html(` +
+
+

Access Denied

+

You do not have permission to access this page. Please contact your administrator to get the "Accounts Manager" role.

+
+
+ `); + return; + } + + var page = frappe.ui.make_app_page({ + parent: wrapper, + title: 'Cashout Requests', + single_column: true + }); + + new FlashCashoutManager(page); +}; + +const CashoutStatus = { + PENDING: "Pending", + PAID: "Paid", + CANCELLED: "Cancelled", + FAILED: "Failed" +}; + +const CASHOUT_STATUS_BADGE_MAP = { + [CashoutStatus.PENDING]: 'cashout-badge-pending', + [CashoutStatus.PAID]: 'cashout-badge-paid', + [CashoutStatus.CANCELLED]: 'cashout-badge-cancelled', + [CashoutStatus.FAILED]: 'cashout-badge-failed' +}; + +function getCashoutStatusBadgeClass(status) { + return CASHOUT_STATUS_BADGE_MAP[status] || 'cashout-badge-pending'; +} + +function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +class FlashCashoutManager { + constructor(page) { + this.page = page; + this.selected_request = null; + this.cashout_requests = []; + this.current_page = 1; + this.page_size = 10; + this.total_pages = 1; + this.total_count = 0; + this.$cache = {}; + this.setup_page(); + } + + setup_page() { + this.create_layout(); + this.cache_elements(); + this.bind_events(); + this.load_cashout_requests(); + } + + cache_elements() { + const main = this.page.main; + this.$cache = { + searchInput: main.find('.search-input'), + requestsLoading: main.find('.requests-loading'), + requestsTable: main.find('.requests-list table'), + noRequests: main.find('.no-requests'), + requestDetails: main.find('.request-details'), + paginationControls: main.find('.pagination-controls'), + requestsTbody: main.find('.requests-tbody'), + searchLoading: main.find('.search-loading'), + searchError: main.find('.search-error'), + filterStatus: main.find('#filter-status') + }; + } + + create_layout() { + this.page.main.html(` + + +
+ +
+
+ + +
+
+ +
+
+ + +
+
+
+ + Cashout Requests +
+ +
+
+ + +
+ + + + + + + + + + + + + + +
UsernamePhoneSend (USD)Receive (JMD)StatusSubmitted
+ +
+
+ + +
+ + + + + + + + +
+ `); + } + + bind_events() { + const main = this.page.main; + + const debouncedSearch = debounce(() => { + if (this.$cache.searchInput.val().trim()) { + this.search(); + } else { + this.$cache.searchError.hide(); + this.load_cashout_requests(); + } + }, 300); + + main.find('.btn-search').on('click', () => this.search()); + this.$cache.searchInput.on('keypress', (e) => { if (e.which === 13) this.search(); }); + this.$cache.searchInput.on('input', debouncedSearch); + + main.find('.btn-refresh').on('click', () => this.load_cashout_requests()); + main.find('.btn-close-details').on('click', () => this.close_details()); + main.find('.btn-record-payment').on('click', () => this.record_payment_entry(this.selected_request)); + + this.$cache.filterStatus.on('change', () => { this.current_page = 1; this.load_cashout_requests(); }); + + main.find('.btn-first-page').on('click', () => this.go_to_page(1)); + main.find('.btn-prev-page').on('click', () => this.go_to_page(this.current_page - 1)); + main.find('.btn-next-page').on('click', () => this.go_to_page(this.current_page + 1)); + main.find('.btn-last-page').on('click', () => this.go_to_page(this.total_pages)); + } + + create_request_row(req) { + const statusBadge = getCashoutStatusBadgeClass(req.status); + const displayStatus = req.status || CashoutStatus.PENDING; + const sendDisplay = req.send != null + ? `USD ${parseFloat(req.send).toLocaleString('en-US', { minimumFractionDigits: 2 })}` + : '-'; + const receiveJmdDisplay = req.receive_jmd != null + ? `JMD ${parseFloat(req.receive_jmd).toLocaleString('en-US', { minimumFractionDigits: 2 })}` + : '-'; + + const row = $(` + + ${req.username || '-'} + ${this.formatPhone(req.phone_number)} + ${sendDisplay} + ${receiveJmdDisplay} + ${displayStatus} + ${this.formatDateTime(req.creation)} + + `); + + row.on('click', () => { + this.page.main.find('.cashout-row').removeClass('selected'); + row.addClass('selected'); + this.show_request_details(req); + }); + + return row; + } + + go_to_page(page) { + if (page < 1 || page > this.total_pages) return; + this.current_page = page; + this.load_cashout_requests(); + } + + load_cashout_requests() { + this.$cache.requestsLoading.show(); + this.$cache.requestsTable.hide(); + this.$cache.noRequests.hide(); + this.$cache.requestDetails.hide(); + this.$cache.paginationControls.hide(); + + frappe.call({ + method: 'admin_panel.api.admin_api.get_cashout_requests', + args: { + status: this.$cache.filterStatus.val(), + page: this.current_page, + page_size: this.page_size + }, + callback: (response) => { + this.$cache.requestsLoading.hide(); + const result = response.message || {}; + this.cashout_requests = result.data || []; + this.total_count = result.total || 0; + this.total_pages = result.total_pages || 1; + this.current_page = result.page || 1; + this.render_requests(); + this.update_pagination(); + }, + error: () => { + this.$cache.requestsLoading.hide(); + frappe.show_alert({ message: 'Failed to load cashout requests', indicator: 'red' }, 5); + } + }); + } + + update_pagination() { + if (this.total_count === 0) { + this.$cache.paginationControls.hide(); + return; + } + + this.$cache.paginationControls.css('display', 'flex'); + + const start = (this.current_page - 1) * this.page_size + 1; + const end = Math.min(this.current_page * this.page_size, this.total_count); + const main = this.page.main; + + main.find('.page-start').text(start); + main.find('.page-end').text(end); + main.find('.total-count').text(this.total_count); + main.find('.current-page').text(this.current_page); + main.find('.total-pages').text(this.total_pages); + + main.find('.btn-first-page, .btn-prev-page').prop('disabled', this.current_page <= 1); + main.find('.btn-next-page, .btn-last-page').prop('disabled', this.current_page >= this.total_pages); + } + + render_requests() { + this.$cache.requestsTbody.empty(); + + if (this.cashout_requests.length === 0) { + this.$cache.requestsTable.hide(); + this.$cache.noRequests.show(); + return; + } + + this.$cache.requestsTable.show(); + this.$cache.noRequests.hide(); + + this.cashout_requests.forEach((req) => { + this.$cache.requestsTbody.append(this.create_request_row(req)); + }); + } + + show_request_details(req) { + this.selected_request = req; + const panel = this.page.main.find('.request-details'); + + // User Information + panel.find('.detail-username').text(req.username || '-'); + panel.find('.detail-fullname').text(req.full_name || '-'); + panel.find('.detail-phone').text(this.formatPhone(req.phone_number) || '-'); + panel.find('.detail-email').text(req.email || '-'); + + // Cashout Information + panel.find('.detail-offer-id').text(req.offer_id || '-'); + panel.find('.detail-wallet-id').text(req.wallet_id || '-'); + panel.find('.detail-send').text( + req.send != null ? `USD ${parseFloat(req.send).toLocaleString('en-US', { minimumFractionDigits: 2 })}` : '-' + ); + panel.find('.detail-flash-fee').text( + req.flash_fee != null ? `USD ${parseFloat(req.flash_fee).toLocaleString('en-US', { minimumFractionDigits: 2 })}` : '-' + ); + panel.find('.detail-exchange-rate').text( + req.exchange_rate != null ? parseFloat(req.exchange_rate).toLocaleString('en-US', { minimumFractionDigits: 4 }) : '-' + ); + panel.find('.detail-receive-jmd').text( + req.receive_jmd != null ? `JMD ${parseFloat(req.receive_jmd).toLocaleString('en-US', { minimumFractionDigits: 2 })}` : '-' + ); + panel.find('.detail-receive-usd').text( + req.receive_usd != null ? `USD ${parseFloat(req.receive_usd).toLocaleString('en-US', { minimumFractionDigits: 2 })}` : '-' + ); + if (req.journal_entry) { + panel.find('.detail-journal-entry').html( + `${req.journal_entry}` + ); + } else { + panel.find('.detail-journal-entry').text('-'); + } + + // Bank Account + if (req.bank_account) { + panel.find('.detail-bank-account').html( + `${req.bank_account}` + ); + } else { + panel.find('.detail-bank-account').text('-'); + } + panel.find('.detail-bank-name').text(req.bank_name || '-'); + panel.find('.detail-account-number').text(req.account_number || '-'); + panel.find('.detail-account-type').text(req.account_type || '-'); + + // Payment Entry section (shown only when recorded) + const paymentSection = panel.find('.detail-payment-entry-section'); + if (req.payment_entry) { + panel.find('.detail-payment-entry').html( + `${req.payment_entry}` + ); + panel.find('.detail-pe-amount').text( + req.pe_paid_amount != null + ? `${req.pe_currency || ''} ${parseFloat(req.pe_paid_amount).toLocaleString('en-US', { minimumFractionDigits: 2 })}`.trim() + : '-' + ); + panel.find('.detail-pe-date').text(req.pe_posting_date ? frappe.datetime.str_to_user(req.pe_posting_date) : '-'); + panel.find('.detail-pe-mode').text(req.pe_mode_of_payment || '-'); + paymentSection.show(); + } else { + paymentSection.hide(); + } + + // Other Information + panel.find('.detail-status').html( + `${req.status || '-'}` + ); + panel.find('.detail-request-id').text(req.name || '-'); + panel.find('.detail-submitted').text(this.formatDateTime(req.creation)); + panel.find('.detail-modified').text(this.formatDateTime(req.modified)); + + // Record Payment Entry button — only for Pending without a payment entry + const recordBtn = panel.find('.btn-record-payment'); + if (req.status === CashoutStatus.PENDING && !req.payment_entry) { + recordBtn.show(); + } else { + recordBtn.hide(); + } + + panel.show(); + + const row = this.page.main.find(`tr[data-request-id="${req.name}"]`); + if (row.length) { + row[0].scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + } + + record_payment_entry(req) { + if (!req) return; + + const sendDisplay = req.send != null + ? `USD ${parseFloat(req.send).toLocaleString('en-US', { minimumFractionDigits: 2 })}` + : 'this cashout'; + + frappe.confirm( + `Are you sure you want to record a payment of ${sendDisplay} for ${req.username}?

This will create a Payment Entry linked to ${req.journal_entry || 'the cashout request'}.`, + () => { + const btn = this.page.main.find('.btn-record-payment'); + btn.prop('disabled', true).html(' Processing...'); + + frappe.call({ + method: 'admin_panel.api.admin_api.record_cashout_payment', + args: { cashout_id: req.name }, + freeze: true, + freeze_message: 'Creating Payment Entry...', + callback: (r) => { + btn.prop('disabled', false).html(' Record Payment Entry'); + const result = r.message || {}; + if (result.success) { + frappe.msgprint({ + title: 'Payment Recorded', + indicator: 'green', + message: result.message || 'Payment Entry created successfully.' + }); + // Update local data and refresh + req.status = CashoutStatus.PAID; + req.payment_entry = result.payment_entry; + this.show_request_details(req); + this.load_cashout_requests(); + } else { + frappe.msgprint({ + title: 'Error', + indicator: 'red', + message: result.error || 'Failed to create Payment Entry.' + }); + } + }, + error: (err) => { + btn.prop('disabled', false).html(' Record Payment Entry'); + const msg = err?.responseJSON?.exception || err?.responseJSON?.message || 'Failed to create Payment Entry'; + frappe.msgprint({ title: 'Error', indicator: 'red', message: msg }); + } + }); + } + ); + } + + close_details() { + this.$cache.requestDetails.hide(); + this.selected_request = null; + } + + search() { + const input = this.$cache.searchInput.val().trim(); + if (!input) { + frappe.show_alert({ message: 'Please enter a username or phone number', indicator: 'orange' }, 3); + return; + } + + this.$cache.searchLoading.show(); + this.$cache.searchError.hide(); + this.$cache.requestDetails.hide(); + + frappe.call({ + method: 'admin_panel.api.admin_api.search_cashout_account', + args: { id: input }, + callback: (res) => { + this.$cache.searchLoading.hide(); + const results = res.message; + if (!results || results.error) { + this.show_search_error(results?.error || 'Account not found'); + return; + } + this.show_search_results(Array.isArray(results) ? results : []); + }, + error: (e) => { + this.$cache.searchLoading.hide(); + this.show_search_error(e.message || 'Account not found'); + } + }); + } + + show_search_results(results) { + this.$cache.requestsTbody.empty(); + this.$cache.searchError.hide(); + this.$cache.paginationControls.hide(); + + if (!results || !results.length) { + this.$cache.noRequests.show(); + this.$cache.requestsTable.hide(); + this.show_search_error('No cashout requests found'); + return; + } + + this.$cache.noRequests.hide(); + this.$cache.requestsTable.show(); + + results.forEach(req => { + this.$cache.requestsTbody.append(this.create_request_row(req)); + }); + } + + show_search_error(msg) { + this.$cache.searchLoading.hide(); + this.$cache.searchError.show(); + this.page.main.find('.error-message').text(msg); + } + + formatPhone(phone) { + if (!phone) return '-'; + return phone.replace(/^(\d{3})(\d{3})(\d{2})(\d{2})$/, '+$1 $2 $3 $4'); + } + + formatDateTime(dt) { + return dt ? frappe.datetime.str_to_user(dt) : '-'; + } +} diff --git a/admin_panel/admin_panel/page/cashout_requests/cashout_requests.json b/admin_panel/admin_panel/page/cashout_requests/cashout_requests.json new file mode 100644 index 0000000..2840e9e --- /dev/null +++ b/admin_panel/admin_panel/page/cashout_requests/cashout_requests.json @@ -0,0 +1,19 @@ +{ + "content": null, + "creation": "2026-04-03 12:00:00", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2026-04-03 12:29:03.124676", + "modified_by": "Administrator", + "module": "Admin Panel", + "name": "cashout-requests", + "owner": "Administrator", + "page_name": "cashout-requests", + "roles": [], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0, + "title": "Cashout Requests" +} \ No newline at end of file diff --git a/admin_panel/admin_panel/setup.py b/admin_panel/admin_panel/setup.py index f091b6c..e516c4e 100644 --- a/admin_panel/admin_panel/setup.py +++ b/admin_panel/admin_panel/setup.py @@ -21,6 +21,13 @@ def sync_pages(): "standard": "Yes", "roles": [], }, + { + "name": "cashout-requests", + "title": "Cashout Requests", + "module": "Admin Panel", + "standard": "Yes", + "roles": [], + }, ] for page_data in pages: diff --git a/admin_panel/fixtures/workspace.json b/admin_panel/fixtures/workspace.json index 13233be..660f804 100644 --- a/admin_panel/fixtures/workspace.json +++ b/admin_panel/fixtures/workspace.json @@ -1,7 +1,7 @@ [ { "charts": [], - "content": "[{\"id\":\"_EYFml0tr6\",\"type\":\"header\",\"data\":{\"text\":\"Admin Panel\",\"col\":12}},{\"id\":\"_F4e1CqrUd\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Alert Users\",\"col\":3}},{\"id\":\"_F4e1CqrUe\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Account Management\",\"col\":3}},{\"id\":\"_bxWkoteNn\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":12}}]", + "content": "[{\"id\":\"_EYFml0tr6\",\"type\":\"header\",\"data\":{\"text\":\"Admin Panel\",\"col\":12}},{\"id\":\"_F4e1CqrUd\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Alert Users\",\"col\":3}},{\"id\":\"_F4e1CqrUe\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Account Management\",\"col\":3}},{\"id\":\"_F4e1CqrUf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Cashout Requests\",\"col\":3}},{\"id\":\"_bxWkoteNn\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":12}}]", "custom_blocks": [], "docstatus": 0, "doctype": "Workspace", @@ -65,6 +65,24 @@ "parenttype": "Workspace", "report_ref_doctype": null, "type": "Link" + }, + { + "dependencies": null, + "description": null, + "hidden": 0, + "icon": "currency", + "is_query_report": 0, + "label": "Cashout Requests", + "link_count": 0, + "link_to": "cashout-requests", + "link_type": "Page", + "onboard": 0, + "only_for": null, + "parent": "Admin Panel", + "parentfield": "links", + "parenttype": "Workspace", + "report_ref_doctype": null, + "type": "Link" } ], "modified": "2024-01-01 00:00:00", @@ -111,6 +129,23 @@ "stats_filter": null, "type": "Page", "url": null + }, + { + "color": "#F59E0B", + "doc_view": "", + "format": "", + "icon": "currency", + "kanban_board": null, + "label": "Cashout Requests", + "link_to": "cashout-requests", + "parent": "Admin Panel", + "parentfield": "shortcuts", + "parenttype": "Workspace", + "report_ref_doctype": null, + "restrict_to_domain": null, + "stats_filter": null, + "type": "Page", + "url": null } ], "title": "Admin Panel"