PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.1.2
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.1.2
2.4.1 2.4.0 2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.2 2.2.1 2.2.0 2.1.2 2.1.1 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 All 68 releases
better-payment / src / blocks / user-dashboard / src / components / preview.js

preview.js in Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More 2.1.2, at src/blocks/user-dashboard/src/components/preview.js

742 lines 54.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Dashboard Preview Component
3 * Renders the actual user dashboard matching layout-1.php structure
4 *
5 * @package Better_Payment
6 */
7
8 import { __ } from '@wordpress/i18n';
9 import { useState, useEffect, useCallback, useRef } from '@wordpress/element';
10 import AnalyticsReport from './AnalyticsReport';
11 import RecurringSubscriptionWidget from './RecurringSubscriptionWidget';
12 import SplitSubscriptionWidget from './SplitSubscriptionWidget';
13 import SubscriptionsTab from './SubscriptionsTab';
14
15 const DashboardSVGIcons = {
16 dashboard: () => (
17 <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
18 <g clipPath="url(#clip0_400_549)">
19 <path d="M4 5C4 4.73478 4.10536 4.48043 4.29289 4.29289C4.48043 4.10536 4.73478 4 5 4H9C9.26522 4 9.51957 4.10536 9.70711 4.29289C9.89464 4.48043 10 4.73478 10 5V9C10 9.26522 9.89464 9.51957 9.70711 9.70711C9.51957 9.89464 9.26522 10 9 10H5C4.73478 10 4.48043 9.89464 4.29289 9.70711C4.10536 9.51957 4 9.26522 4 9V5Z" stroke="#48506D" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
20 <path d="M4 15C4 14.7348 4.10536 14.4804 4.29289 14.2929C4.48043 14.1054 4.73478 14 5 14H9C9.26522 14 9.51957 14.1054 9.70711 14.2929C9.89464 14.4804 10 14.7348 10 15V19C10 19.2652 9.89464 19.5196 9.70711 19.7071C9.51957 19.8946 9.26522 20 9 20H5C4.73478 20 4.48043 19.8946 4.29289 19.7071C4.10536 19.5196 4 19.2652 4 19V15Z" stroke="#48506D" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
21 <path d="M14 15C14 14.7348 14.1054 14.4804 14.2929 14.2929C14.4804 14.1054 14.7348 14 15 14H19C19.2652 14 19.5196 14.1054 19.7071 14.2929C19.8946 14.4804 20 14.7348 20 15V19C20 19.2652 19.8946 19.5196 19.7071 19.7071C19.5196 19.8946 19.2652 20 19 20H15C14.7348 20 14.4804 19.8946 14.2929 19.7071C14.1054 19.5196 14 19.2652 14 19V15Z" stroke="#48506D" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
22 <path d="M14 7H20" stroke="#48506D" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
23 <path d="M17 4V10" stroke="#48506D" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
24 </g>
25 <defs>
26 <clipPath id="clip0_400_549">
27 <rect width="24" height="24" fill="white" />
28 </clipPath>
29 </defs>
30 </svg>
31 ),
32 transactions: () => (
33 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
34 <path fillRule="evenodd" clipRule="evenodd" d="M7.09878 1.24992C7.14683 1.24994 7.19559 1.24996 7.24508 1.24996H16.7551C16.8045 1.24996 16.8533 1.24994 16.9014 1.24992C17.9181 1.24947 18.6178 1.24917 19.2072 1.45422C20.3201 1.84149 21.1842 2.73714 21.5547 3.86546L20.8421 4.09942L21.5547 3.86546C21.7507 4.46258 21.7505 5.17242 21.7501 6.22642C21.7501 6.27359 21.7501 6.32145 21.7501 6.37002V20.3742C21.7501 21.8394 20.0231 22.7117 18.8857 21.6709C18.8062 21.5981 18.694 21.5981 18.6145 21.6709L18.1314 22.1129C17.2032 22.9623 15.7969 22.9623 14.8688 22.1129C14.5138 21.7881 13.9864 21.7881 13.6314 22.1129C12.7032 22.9623 11.2969 22.9623 10.3688 22.1129C10.0138 21.7881 9.48637 21.7881 9.13138 22.1129C8.20319 22.9623 6.79694 22.9623 5.86875 22.1129L5.38566 21.6709C5.30618 21.5981 5.19395 21.5981 5.11448 21.6709C3.97705 22.7117 2.25007 21.8394 2.25007 20.3742V6.37002C2.25007 6.32145 2.25005 6.2736 2.25003 6.22643C2.24965 5.17242 2.24939 4.46259 2.44545 3.86546C2.81591 2.73714 3.68002 1.84149 4.79298 1.45422C5.3823 1.24917 6.08203 1.24947 7.09878 1.24992ZM7.24508 2.74996C6.024 2.74996 5.6034 2.76045 5.28593 2.87091C4.62655 3.10035 4.09919 3.63716 3.8706 4.33338C3.75951 4.67171 3.75007 5.11784 3.75007 6.37002V20.3742C3.75007 20.4932 3.80999 20.566 3.88517 20.6007C3.92434 20.6189 3.96264 20.6235 3.99456 20.6193C4.0227 20.6155 4.05911 20.6034 4.10185 20.5643C4.75453 19.967 5.74561 19.967 6.39828 20.5643L6.88138 21.0063C7.23637 21.3312 7.76377 21.3312 8.11875 21.0063C9.04694 20.157 10.4532 20.157 11.3814 21.0063C11.7364 21.3312 12.2638 21.3312 12.6188 21.0063C13.5469 20.157 14.9532 20.157 15.8814 21.0063C16.2364 21.3312 16.7638 21.3312 17.1188 21.0063L17.6019 20.5643C18.2545 19.967 19.2456 19.967 19.8983 20.5643C19.941 20.6034 19.9774 20.6155 20.0056 20.6193C20.0375 20.6235 20.0758 20.6189 20.115 20.6007C20.1901 20.566 20.2501 20.4932 20.2501 20.3742V6.37002C20.2501 5.11784 20.2406 4.67171 20.1295 4.33338C19.9009 3.63716 19.3736 3.10035 18.7142 2.87091C18.3967 2.76045 17.9761 2.74996 16.7551 2.74996H7.24508ZM6.25007 7.49996C6.25007 7.08575 6.58585 6.74996 7.00007 6.74996H7.50007C7.91428 6.74996 8.25007 7.08575 8.25007 7.49996C8.25007 7.91417 7.91428 8.24996 7.50007 8.24996H7.00007C6.58585 8.24996 6.25007 7.91417 6.25007 7.49996ZM9.75007 7.49996C9.75007 7.08575 10.0859 6.74996 10.5001 6.74996H17.0001C17.4143 6.74996 17.7501 7.08575 17.7501 7.49996C17.7501 7.91417 17.4143 8.24996 17.0001 8.24996H10.5001C10.0859 8.24996 9.75007 7.91417 9.75007 7.49996ZM6.25007 11C6.25007 10.5857 6.58585 10.25 7.00007 10.25H7.50007C7.91428 10.25 8.25007 10.5857 8.25007 11C8.25007 11.4142 7.91428 11.75 7.50007 11.75H7.00007C6.58585 11.75 6.25007 11.4142 6.25007 11ZM9.75007 11C9.75007 10.5857 10.0859 10.25 10.5001 10.25H17.0001C17.4143 10.25 17.7501 10.5857 17.7501 11C17.7501 11.4142 17.4143 11.75 17.0001 11.75H10.5001C10.0859 11.75 9.75007 11.4142 9.75007 11ZM6.25007 14.5C6.25007 14.0857 6.58585 13.75 7.00007 13.75H7.50007C7.91428 13.75 8.25007 14.0857 8.25007 14.5C8.25007 14.9142 7.91428 15.25 7.50007 15.25H7.00007C6.58585 15.25 6.25007 14.9142 6.25007 14.5ZM9.75007 14.5C9.75007 14.0857 10.0859 13.75 10.5001 13.75H17.0001C17.4143 13.75 17.7501 14.0857 17.7501 14.5C17.7501 14.9142 17.4143 15.25 17.0001 15.25H10.5001C10.0859 15.25 9.75007 14.9142 9.75007 14.5Z" fill="#667085" />
35 </svg>
36 ),
37 subscriptions: () => (
38 <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
39 <g clipPath="url(#clip0_400_558)">
40 <path d="M3 8V12.172C3.00011 12.7024 3.2109 13.211 3.586 13.586L9.296 19.296C9.74795 19.7479 10.3609 20.0017 11 20.0017C11.6391 20.0017 12.252 19.7479 12.704 19.296L16.296 15.704C16.7479 15.252 17.0017 14.6391 17.0017 14C17.0017 13.3609 16.7479 12.748 16.296 12.296L10.586 6.586C10.211 6.2109 9.70239 6.00011 9.172 6H5C4.46957 6 3.96086 6.21071 3.58579 6.58579C3.21071 6.96086 3 7.46957 3 8Z" stroke="#2B2748" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
41 <path d="M18 19L19.592 17.408C20.4958 16.5041 21.0035 15.2782 21.0035 14C21.0035 12.7218 20.4958 11.4959 19.592 10.592L15 6" stroke="#2B2748" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
42 <path d="M6.99828 10H6.98828" stroke="#2B2748" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
43 </g>
44 <defs>
45 <clipPath id="clip0_400_558">
46 <rect width="24" height="24" fill="white" />
47 </clipPath>
48 </defs>
49 </svg>
50 ),
51 };
52
53 /**
54 * Map transaction status to display label and background color.
55 * Mirrors PHP Helper::get_type_by_transaction_status($status, 'v2') and get_color_by_transaction_status($status, 'v2')
56 */
57 function getBpStatusInfo( status ) {
58 const s = status || '';
59 if ( [ 'paid', 'Completed', 'completed', 'success' ].includes( s ) ) {
60 return { label: 'Completed', color: '#0ECA86' };
61 }
62 if ( s === 'refunded' ) {
63 return { label: 'Refunded', color: '#FF0202' };
64 }
65 // pending, Pending, unpaid, incomplete, Incomplete, empty → Incomplete
66 return { label: 'Incomplete', color: '#FFDA15' };
67 }
68
69 /**
70 * Format date to match WordPress site date-only format: 'F j, Y'
71 * Example: "April 9, 2026"
72 */
73 function formatBpDateOnly( dateStr ) {
74 if ( ! dateStr ) return 'N/A';
75 const d = new Date( dateStr );
76 const month = d.toLocaleString( 'en-US', { month: 'long' } );
77 const day = d.getDate();
78 const year = d.getFullYear();
79 return `${ month } ${ day }, ${ year }`;
80 }
81
82 /**
83 * Format date to match WordPress site date/time format: 'F j, Y g:i a'
84 * Example: "April 9, 2026 2:25 pm"
85 */
86 function formatBpDate( dateStr ) {
87 if ( ! dateStr ) return 'N/A';
88 const d = new Date( dateStr );
89 const month = d.toLocaleString( 'en-US', { month: 'long' } );
90 const day = d.getDate();
91 const year = d.getFullYear();
92 let hours = d.getHours();
93 const mins = String( d.getMinutes() ).padStart( 2, '0' );
94 const ampm = hours >= 12 ? 'pm' : 'am';
95 hours = hours % 12 || 12;
96 return `${ month } ${ day }, ${ year } ${ hours }:${ mins } ${ ampm }`;
97 }
98
99 export default function DashboardPreview({ attributes }) {
100 const {
101 sidebarShow,
102 avatarShow,
103 usernameShow,
104 dashboardShow,
105 transactionsShow,
106 subscriptionsShow,
107 headerShow,
108 dashboardLabel,
109 transactionLabel,
110 subscriptionLabel,
111 refreshStatsLabel,
112 noItemsLabel,
113 dashboardTransactionSummaryShow,
114 dashboardAnalyticsReportShow,
115 dashboardRecentTransactionsShow,
116 dashboardRecurringSubscriptionShow,
117 dashboardSplitSubscriptionShow,
118 transactionTableNameShow,
119 transactionTableEmailAddressShow,
120 transactionTableAmountShow,
121 transactionTablePaymentTypeShow,
122 transactionTableTransactionIdShow,
123 transactionTableSourceShow,
124 transactionTableStatusShow,
125 transactionTableDateShow,
126 dashboardTotalAmountLabel,
127 dashboardCompletedAmountLabel,
128 dashboardIncompleteAmountLabel,
129 dashboardRefundedAmountLabel,
130 dashboardViewAllLabel,
131 dashboardAnalyticsReportsLabel,
132 dashboardRecentTransactionsLabel,
133 transactionTableNameLabel,
134 transactionTableEmailAddressLabel,
135 transactionTableAmountLabel,
136 transactionTablePaymentTypeLabel,
137 transactionTableTransactionIdLabel,
138 transactionTableSourceLabel,
139 transactionTableStatusLabel,
140 transactionTableDateLabel,
141 } = attributes;
142
143 const blockData = window.betterPaymentBlockData || {};
144 const currentUser = blockData.currentUser || { user_email: '', user_login: '', user_avatar_url: '' };
145 const analytics = blockData.userAnalytics || {};
146 const assetUrl = blockData.assetUrl || blockData.assetsUrl || '';
147 const proAssets = blockData.proAssets || {};
148 const proEnabled = blockData.proEnabled === true;
149 const analyticsChartData = blockData.analyticsChartData || null;
150 const restUrl = blockData.restUrl || '';
151 const restNonce = blockData.restNonce || '';
152 const initMeta = blockData.userTransactionsMeta || { total: 0, pages: 1, perPage: 20 };
153
154 // Paginated transaction state — initialized from block data (page 1)
155 const [txRows, setTxRows] = useState(blockData.userTransactions || []);
156 const [txPage, setTxPage] = useState(1);
157 const [txMeta, setTxMeta] = useState(initMeta);
158 const [txLoading, setTxLoading] = useState(false);
159 const txMounted = useRef(false);
160
161 const fetchTxPage = useCallback((page) => {
162 if ( ! restUrl ) return;
163 setTxLoading(true);
164 fetch(`${restUrl}?tab=transactions&page=${page}&per_page=${txMeta.perPage || 20}`, {
165 headers: { 'X-WP-Nonce': restNonce },
166 })
167 .then(r => r.json())
168 .then(data => {
169 setTxRows(data.transactions || []);
170 setTxPage(data.page || page);
171 setTxMeta({ total: data.total, pages: data.pages, perPage: data.per_page });
172 })
173 .finally(() => setTxLoading(false));
174 }, [restUrl, restNonce, txMeta.perPage]);
175
176 useEffect(() => {
177 if ( ! txMounted.current ) {
178 txMounted.current = true;
179 return;
180 }
181 fetchTxPage(txPage);
182 }, [txPage]); // eslint-disable-line react-hooks/exhaustive-deps
183
184 // Paginated subscription state — initialized from block data (page 1)
185 const initSubMeta = blockData.userSubscriptionsMeta || { total: 0, pages: 1, perPage: 20 };
186 const [subRows, setSubRows] = useState(blockData.userSubscriptions || []);
187 const [subPage, setSubPage] = useState(1);
188 const [subMeta, setSubMeta] = useState(initSubMeta);
189 const subMounted = useRef(false);
190
191 const fetchSubPage = useCallback((page) => {
192 if ( ! restUrl ) return;
193 fetch(`${restUrl}?tab=subscriptions&page=${page}&per_page=${subMeta.perPage || 20}`, {
194 headers: { 'X-WP-Nonce': restNonce },
195 })
196 .then(r => r.json())
197 .then(data => {
198 setSubRows(data.transactions || []);
199 setSubPage(data.page || page);
200 setSubMeta({ total: data.total, pages: data.pages, perPage: data.per_page });
201 });
202 }, [restUrl, restNonce, subMeta.perPage]);
203
204 useEffect(() => {
205 if ( ! subMounted.current ) {
206 subMounted.current = true;
207 return;
208 }
209 fetchSubPage(subPage);
210 }, [subPage]); // eslint-disable-line react-hooks/exhaustive-deps
211
212 // Subscription data for the Dashboard tab summary widgets (uses first page from block data)
213 const allSubscriptions = blockData.userSubscriptions || [];
214 const recurringSubscriptions = allSubscriptions.filter(tx => !parseInt(tx.is_payment_split_payment || 0, 10));
215 const splitSubscriptions = allSubscriptions.filter(tx => !!parseInt(tx.is_payment_split_payment || 0, 10));
216
217 const [activeTab, setActiveTab] = useState(
218 dashboardShow ? 'dashboard' : transactionsShow ? 'transactions' : subscriptionsShow ? 'subscriptions' : 'dashboard'
219 );
220
221 return (
222 <div className="better-payment">
223 <div className="better-payment-user-dashboard-container bp--section-wrapper flex gap-6 min-h-full">
224 <div className="bp-overlay"></div>
225
226 {/* Sidebar */}
227 {sidebarShow && (
228 <div className="better-payment-user-dashboard-sidebar user-dashboard-sidebar bp--sidebar-wrapper bp-hidden-xs">
229 <div className="bp--author">
230 {avatarShow && (
231 <div>
232 <img
233 src={currentUser.user_avatar_url || currentUser.avatar || 'https://www.gravatar.com/avatar/?d=mm'}
234 alt={currentUser.user_login || currentUser.login || 'user'}
235 style={{ width: '32px', height: '32px', minWidth: '32px', borderRadius: '50%', objectFit: 'cover', flexShrink: 0, display: 'block' }}
236 />
237 </div>
238 )}
239 {usernameShow && (
240 <h5 className="user-name">
241 {currentUser.user_login || currentUser.login || 'Admin'}
242 </h5>
243 )}
244 </div>
245
246 <div className="bp--sidebar-nav-list">
247 {dashboardShow && (
248 <div
249 className={`bp--sidebar-nav dashboard-tab ${activeTab === 'dashboard' ? 'active' : ''}`}
250 onClick={() => setActiveTab('dashboard')}
251 >
252 <span className="bp--nav-icon">
253 <DashboardSVGIcons.dashboard />
254 </span>
255 <span className="bp--nav-text">{dashboardLabel || __('Dashboard', 'better-payment')}</span>
256 </div>
257 )}
258
259 {transactionsShow && (
260 <div
261 className={`bp--sidebar-nav transactions-tab ${!dashboardShow && activeTab === 'transactions' ? 'active' : activeTab === 'transactions' ? 'active' : ''}`}
262 onClick={() => setActiveTab('transactions')}
263 >
264 <span className="bp--nav-icon">
265 <DashboardSVGIcons.transactions />
266 </span>
267 <span className="bp--nav-text">{transactionLabel || __('Transactions', 'better-payment')}</span>
268 </div>
269 )}
270
271 {subscriptionsShow && (
272 <div
273 className={`bp--sidebar-nav subscriptions-tab ${(!dashboardShow && !transactionsShow) || activeTab === 'subscriptions' ? 'active' : ''}`}
274 onClick={() => setActiveTab('subscriptions')}
275 >
276 <span className="bp--nav-icon">
277 <DashboardSVGIcons.subscriptions />
278 </span>
279 <span className="bp--nav-text">{subscriptionLabel || __('Subscriptions', 'better-payment')}</span>
280 </div>
281 )}
282 </div>
283 </div>
284 )}
285
286 {/* Main Content */}
287 <div className="bp--db-main-wrapper">
288 {/* Dashboard Tab */}
289 {dashboardShow && activeTab === 'dashboard' && (
290 <div className="bp--tab-conetnt-wrapper dashboard-tab-wrapper">
291 {headerShow && (
292 <div className="better-payment-user-dashboard-header bp--db-header bp-dashboard-header flex items-center justify-center">
293 {sidebarShow && (
294 <div className="bp-dashboard-hamburger padding-0 bp-visible-xs">
295 <svg fill="none" viewBox="0 0 24 24" width="25" xmlns="http://www.w3.org/2000/svg">
296 <path clipRule="evenodd" d="m3 8c0-.55228.44772-1 1-1h16c.5523 0 1 .44772 1 1s-.4477 1-1 1h-16c-.55228 0-1-.44772-1-1zm0 4c0-.5523.44772-1 1-1h16c.5523 0 1 .4477 1 1s-.4477 1-1 1h-16c-.55228 0-1-.4477-1-1zm0 4c0-.5523.44772-1 1-1h8c.5523 0 1 .4477 1 1s-.4477 1-1 1h-8c-.55228 0-1-.4477-1-1z" fill="rgb(0,0,0)" fillRule="evenodd" />
297 </svg>
298 </div>
299 )}
300 <h2>{dashboardLabel || __('Dashboard', 'better-payment')}</h2>
301 <button className="primary-btn bp-hidden-xs">
302 <a href="#" onClick={(e) => { e.preventDefault(); window.location.reload(); }}>
303 {refreshStatsLabel || __('Refresh Stats', 'better-payment')}
304 </a>
305 </button>
306 </div>
307 )}
308
309 <div className="bp-dashboard_wrapper">
310 {/* Transaction Summary */}
311 {dashboardTransactionSummaryShow && (
312 <div className="bp-amount_wrapper">
313 <div className="bp-row">
314 <div className="bp-col_4 bp-col">
315 <div className="bp-amount">
316 <h4 className="mb-0 pb-0">
317 {dashboardTotalAmountLabel || __('Total Amount', 'better-payment')}
318 </h4>
319 <div className="bp-amount_price mb-0 pb-0">
320 <span>${parseFloat(analytics.total_transactions_amount || 0).toFixed(2)}</span>
321 </div>
322 <div className="bp-transaction">
323 <span className="bp-transaction_title">No of Transaction:</span>
324 <span className="bp-transaction_number"> {analytics.total_transactions_count || 0}</span>
325 </div>
326 </div>
327 </div>
328
329 <div className="bp-col_4 bp-col">
330 <div className="bp-amount">
331 <h4 className="mb-0 pb-0">
332 {dashboardCompletedAmountLabel || __('Completed Amount', 'better-payment')}
333 </h4>
334 <div className="bp-amount_price mb-0 pb-0">
335 <span>${parseFloat(analytics.completed_transactions_amount || 0).toFixed(2)}</span>
336 </div>
337 <div className="bp-transaction">
338 <span className="bp-transaction_title">No of Transaction:</span>
339 <span className="bp-transaction_number"> {analytics.completed_transactions_count || 0}</span>
340 </div>
341 </div>
342 </div>
343
344 <div className="bp-col_4 bp-col">
345 <div className="bp-amount">
346 <h4 className="mb-0 pb-0">
347 {dashboardIncompleteAmountLabel || __('Incomplete Amount', 'better-payment')}
348 </h4>
349 <div className="bp-amount_price mb-0 pb-0">
350 <span>${parseFloat(analytics.incomplete_transactions_amount || 0).toFixed(2)}</span>
351 </div>
352 <div className="bp-transaction">
353 <span className="bp-transaction_title">No of Transaction:</span>
354 <span className="bp-transaction_number"> {analytics.incomplete_transactions_count || 0}</span>
355 </div>
356 </div>
357 </div>
358
359 <div className="bp-col_4 bp-col">
360 <div className="bp-amount">
361 <h4 className="mb-0 pb-0">
362 {dashboardRefundedAmountLabel || __('Refunded Amount', 'better-payment')}
363 </h4>
364 <div className="bp-amount_price mb-0 pb-0">
365 <span>${parseFloat(analytics.refunded_transactions_amount || 0).toFixed(2)}</span>
366 </div>
367 <div className="bp-transaction">
368 <span className="bp-transaction_title">No of Transaction:</span>
369 <span className="bp-transaction_number"> {analytics.refunded_transactions_count || 0}</span>
370 </div>
371 </div>
372 </div>
373 </div>
374 </div>
375 )}
376
377 {/* Analytics chart (75%) + Recent Transactions (25%) — siblings in one row, matching template-dashboard-tab.php */}
378 {(dashboardAnalyticsReportShow || dashboardRecentTransactionsShow) && (
379 <div className="bp-analytics_chart-wrapper">
380 <div className="bp-row">
381 {dashboardAnalyticsReportShow && (
382 <div className="bp-col_10 bp-col">
383 <AnalyticsReport
384 proEnabled={proEnabled}
385 proAssets={proAssets}
386 analyticsChartData={analyticsChartData}
387 bp_settings={attributes}
388 />
389 </div>
390 )}
391
392 {dashboardRecentTransactionsShow && (
393 <div className="bp-col_4 bp-col">
394 <div className="bp-recent_box">
395 <div className="bp-recent_header flex justify-between items-center">
396 <div className="flex gap-2 items-center">
397 <span>
398 <svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
399 <path d="M13.9586 2.16663H6.03361C5.06788 2.16663 4.58502 2.16663 4.19558 2.30213C3.45711 2.55909 2.87733 3.15595 2.62772 3.91618C2.49609 4.31708 2.49609 4.81417 2.49609 5.80834V17.4785C2.49609 18.1937 3.31692 18.5731 3.83617 18.098C4.14124 17.8188 4.60095 17.8188 4.90601 18.098L5.30859 18.4664C5.84325 18.9556 6.64894 18.9556 7.18359 18.4664C7.71825 17.9771 8.52394 17.9771 9.05859 18.4664C9.59325 18.9556 10.3989 18.9556 10.9336 18.4664C11.4682 17.9771 12.2739 17.9771 12.8086 18.4664C13.3432 18.9556 14.1489 18.9556 14.6836 18.4664L15.0862 18.098C15.3912 17.8188 15.8509 17.8188 16.156 18.098C16.6753 18.5731 17.4961 18.1937 17.4961 17.4785V5.80834C17.4961 4.81417 17.4961 4.31708 17.3645 3.91618C17.1149 3.15595 16.5351 2.55909 15.7966 2.30213C15.4072 2.16663 14.9243 2.16663 13.9586 2.16663Z" stroke="#475467" strokeWidth="1.25" />
400 <path d="M8.74609 9.66663L14.1628 9.66663" stroke="#475467" strokeWidth="1.25" strokeLinecap="round" />
401 <path d="M5.8291 9.66663H6.24577" stroke="#475467" strokeWidth="1.25" strokeLinecap="round" />
402 <path d="M5.8291 6.75H6.24577" stroke="#475467" strokeWidth="1.25" strokeLinecap="round" />
403 <path d="M5.8291 12.5834H6.24577" stroke="#475467" strokeWidth="1.25" strokeLinecap="round" />
404 <path d="M8.74609 6.75H14.1628" stroke="#475467" strokeWidth="1.25" strokeLinecap="round" />
405 <path d="M8.74609 12.5834H14.1628" stroke="#475467" strokeWidth="1.25" strokeLinecap="round" />
406 </svg>
407 </span>
408 <h3 className="bp-recent_header-title">
409 {dashboardRecentTransactionsLabel || __('Recent Transactions', 'better-payment')}
410 </h3>
411 </div>
412 <a href="#" className="bp-view_all-btn is-hidden">
413 {dashboardViewAllLabel || __('View All', 'better-payment')}
414 </a>
415 </div>
416
417 <div className="bp-recent_body">
418 <div className="bp-th flex items-center">
419 <div className="w-195">
420 <h5>Transaction</h5>
421 </div>
422 <div className="w-80">
423 <h5>Amount</h5>
424 </div>
425 </div>
426
427 <div className="bp-product_scroll">
428 {txRows && txRows.length > 0 ? (
429 txRows.slice(0, 5).map((txn, index) => (
430 <div key={index} className="bp-td flex items-center">
431 <div className="td-product flex items-start w-195 flex-col">
432 <div className="td-product_logo">
433 <img
434 src={txn.source === 'paypal' ? assetUrl + '/img/paypal.png' : assetUrl + '/img/' + txn.source + '.svg'}
435 title={txn.source.toUpperCase()}
436 alt={txn.source}
437 />
438 </div>
439 <div className="td-product_info">
440 <h4 className="td-product_name">
441 {formatBpDateOnly( txn.payment_date )}
442 </h4>
443 </div>
444 </div>
445 <div className="td-product_price w-80">
446 <span>{txn.currency} {parseFloat(txn.amount).toFixed(2)}</span>
447 </div>
448 </div>
449 ))
450 ) : (
451 <div>
452 <p className="bp-no_subscription-text">{noItemsLabel || __('No records found!', 'better-payment')}</p>
453 </div>
454 )}
455 </div>
456 </div>
457 </div>
458 </div>
459 )}
460 </div>
461 </div>
462 )}
463
464 {/* Subscriptions Section (Pro only) — both recurring and split in one wrapper, matching template-dashboard-tab.php */}
465 {(dashboardRecurringSubscriptionShow || dashboardSplitSubscriptionShow) && (
466 <div className="bp-subscription_wrapper">
467 <div className="bp-row">
468 {dashboardRecurringSubscriptionShow && (
469 <div className="bp-col_7 bp-col">
470 <RecurringSubscriptionWidget
471 proEnabled={proEnabled}
472 proAssets={proAssets}
473 subscriptions={recurringSubscriptions}
474 title={attributes.dashboardRecurringSubscriptionsLabel || 'Recurring Subscriptions'}
475 viewAllLabel={attributes.dashboardViewAllLabel}
476 noItemsLabel={attributes.noItemsLabel}
477 />
478 </div>
479 )}
480
481 {dashboardSplitSubscriptionShow && (
482 <div className="bp-col_7 bp-col">
483 <SplitSubscriptionWidget
484 proEnabled={proEnabled}
485 proAssets={proAssets}
486 subscriptions={splitSubscriptions}
487 title={attributes.dashboardSplitSubscriptionsLabel || 'Split Subscriptions'}
488 viewAllLabel={attributes.dashboardViewAllLabel}
489 noItemsLabel={attributes.noItemsLabel}
490 />
491 </div>
492 )}
493 </div>
494 </div>
495 )}
496 </div>
497 </div>
498 )}
499
500 {/* Transactions Tab */}
501 {transactionsShow && activeTab === 'transactions' && (
502 <div className="bp--tab-conetnt-wrapper transactions-tab-wrapper">
503 {headerShow && (
504 <div className="better-payment-user-dashboard-header bp--db-header bp-dashboard-header flex items-center justify-center">
505 {sidebarShow && (
506 <div className="bp-dashboard-hamburger padding-0 bp-visible-xs">
507 <svg fill="none" viewBox="0 0 24 24" width="25" xmlns="http://www.w3.org/2000/svg">
508 <path clipRule="evenodd" d="m3 8c0-.55228.44772-1 1-1h16c.5523 0 1 .44772 1 1s-.4477 1-1 1h-16c-.55228 0-1-.44772-1-1zm0 4c0-.5523.44772-1 1-1h16c.5523 0 1 .4477 1 1s-.4477 1-1 1h-16c-.55228 0-1-.4477-1-1zm0 4c0-.5523.44772-1 1-1h8c.5523 0 1 .4477 1 1s-.4477 1-1 1h-8c-.55228 0-1-.4477-1-1z" fill="rgb(0,0,0)" fillRule="evenodd" />
509 </svg>
510 </div>
511 )}
512 <h2>{transactionLabel || __('Transactions', 'better-payment')}</h2>
513 </div>
514 )}
515
516 <div className="bp--body-content">
517 <div className="bp--table-main-wrapper">
518 <div className="bp--table-wrapper transaction better-payment-user-dashboard-table">
519 <div className="better-payment-user-dashboard-table-header bp--table-header bp-min_width-1300 flex justify-between gap-1">
520 {transactionTableNameShow && (
521 <div className="th user-name details">
522 <h5>{transactionTableNameLabel || __('Name', 'better-payment')}</h5>
523 </div>
524 )}
525 {transactionTableEmailAddressShow && (
526 <div className="th details email">
527 <h5>{transactionTableEmailAddressLabel || __('Email Address', 'better-payment')}</h5>
528 </div>
529 )}
530 {transactionTableAmountShow && (
531 <div className="th details amount">
532 <h5>{transactionTableAmountLabel || __('Amount', 'better-payment')}</h5>
533 </div>
534 )}
535 {transactionTablePaymentTypeShow && (
536 <div className="th details">
537 <h5>{transactionTablePaymentTypeLabel || __('Payment Type', 'better-payment')}</h5>
538 </div>
539 )}
540 {transactionTableTransactionIdShow && (
541 <div className="th details transaction">
542 <h5>{transactionTableTransactionIdLabel || __('Transaction ID', 'better-payment')}</h5>
543 </div>
544 )}
545 {transactionTableSourceShow && (
546 <div className="th details flex justify-center">
547 <h5>{transactionTableSourceLabel || __('Source', 'better-payment')}</h5>
548 </div>
549 )}
550 {transactionTableStatusShow && (
551 <div className="th details flex justify-center">
552 <h5>{transactionTableStatusLabel || __('Status', 'better-payment')}</h5>
553 </div>
554 )}
555 {transactionTableDateShow && (
556 <div className="th details flex justify-center">
557 <h5>{transactionTableDateLabel || __('Date', 'better-payment')}</h5>
558 </div>
559 )}
560 </div>
561
562 <div style={{ position: 'relative', opacity: txLoading ? 0.5 : 1 }}>
563 {txRows && txRows.length > 0 ? (
564 txRows.map((txn, index) => {
565 // customer_name and customer_email are pre-computed by PHP (BlockManager)
566 // and by the REST API (UserAPI) — use them directly.
567 const customerName = txn.customer_name || '';
568 const customerEmail = txn.customer_email || '';
569 const paymentType = txn.is_subscription || (() => {
570 const ffi = txn.form_fields_info || {};
571 return ffi.subscription_id ? 'Subscription' : 'One Time';
572 })();
573 const { label: statusLabel, color: statusColor } = getBpStatusInfo( txn.status );
574
575 return (
576 <div key={index} className="better-payment-user-dashboard-table-body bp--table-body bp-min_width-1300 flex items-center justify-between gap-1">
577 {transactionTableNameShow && (
578 <div className="td details user-name flex items-center gap-3">
579 <p>{customerName || 'N/A'}</p>
580 </div>
581 )}
582 {transactionTableEmailAddressShow && (
583 <div className="td details email flex items-center gap-3">
584 <p>
585 <span id={`bp_email_copy_clipboard_input_${index + 1}`}>
586 {customerEmail || 'N/A'}
587 </span>
588 <span
589 id={`bp_email_copy_clipboard_${index + 1}`}
590 className="bp-icon bp-copy-square bp-email-copy-clipboard"
591 title="Copy"
592 data-bp_txn_counter={index + 1}
593 />
594 </p>
595 </div>
596 )}
597 {transactionTableAmountShow && (
598 <div className="td details amount flex items-center gap-3">
599 <p>{txn.currency} {parseFloat(txn.amount).toFixed(2)}</p>
600 </div>
601 )}
602 {transactionTablePaymentTypeShow && (
603 <div className="td details flex items-center gap-3">
604 <p>{paymentType}</p>
605 </div>
606 )}
607 {transactionTableTransactionIdShow && (
608 <div className="td details transaction flex items-center gap-3">
609 {txn.transaction_id ? (
610 <p>
611 <span id={`bp_copy_clipboard_input_${index + 1}`}>
612 {txn.transaction_id}
613 </span>
614 <span
615 id={`bp_copy_clipboard_${index + 1}`}
616 className="bp-icon bp-copy-square bp-copy-clipboard"
617 title="Copy"
618 data-bp_txn_counter={index + 1}
619 />
620 </p>
621 ) : null}
622 </div>
623 )}
624 {transactionTableSourceShow && (
625 <div className="td details flex justify-center gap-3">
626 <img
627 className="source--img"
628 src={txn.source === 'paypal' ? assetUrl + '/img/paypal.png' : assetUrl + '/img/' + txn.source + '.svg'}
629 title={txn.source ? txn.source.toUpperCase() : ''}
630 alt={txn.source || ''}
631 />
632 </div>
633 )}
634 {transactionTableStatusShow && (
635 <div className="td details flex justify-center gap-3">
636 <p data-id={txn.id}>
637 <span style={{ color: '#fff', padding: '7px 15px', borderRadius: '20px', background: statusColor }}>
638 {txn.status_label || statusLabel}
639 </span>
640 </p>
641 </div>
642 )}
643 {transactionTableDateShow && (
644 <div className="td details flex justify-center gap-3">
645 <p>{formatBpDate( txn.payment_date )}</p>
646 </div>
647 )}
648 </div>
649 );
650 })
651 ) : (
652 <div className="flex justify-center m-5">
653 <p className="bp-no_subscription-text">{noItemsLabel || __('No records found!', 'better-payment')}</p>
654 </div>
655 )}
656 </div>
657
658 {txMeta.pages > 1 && (
659 <div className="bp--pagination pagination">
660 <ul>
661 <li>
662 <a
663 href="#"
664 className={`pagination-previous bp--prev${txPage <= 1 ? ' disabled' : ''}`}
665 aria-disabled={txPage <= 1}
666 onClick={(e) => { e.preventDefault(); if (txPage > 1) setTxPage(p => p - 1); }}
667 >
668 <i className="bp-icon bp-caret-left"></i>
669 </a>
670 </li>
671 {(() => {
672 const total = txMeta.pages;
673 const items = [];
674 const rendered = {};
675 let ellipsisLeft = false;
676 let ellipsisRight = false;
677
678 for (let p = 1; p <= total; p++) {
679 const inWindow = p === 1 || p === total || (p >= txPage - 2 && p <= txPage + 2);
680 if (total <= 7 || inWindow) {
681 const isActive = p === txPage;
682 items.push(
683 <li key={p}>
684 <a href="#" className={`pagination-link bp--page-num${isActive ? ' is-current active' : ''}`} aria-label={`Page ${p}`}
685 onClick={(e) => { e.preventDefault(); if (!isActive) setTxPage(p); }}> {p} </a>
686 </li>
687 );
688 rendered[p] = true;
689 } else {
690 const inLeftGap = p < txPage - 2 && !ellipsisLeft;
691 const inRightGap = p > txPage + 2 && !ellipsisRight;
692 if (inLeftGap) {
693 const mid = Math.max(1, Math.round((2 + (txPage - 3)) / 2));
694 items.push(<li key={`el-l`}><a href="#" className="pagination-link bp--page-num bp--ellipsis" data-page={mid} title={`Jump to page ${mid}`} onClick={(e) => { e.preventDefault(); setTxPage(mid); }}>...</a></li>);
695 ellipsisLeft = true;
696 } else if (inRightGap) {
697 const mid = Math.min(total, Math.round(((txPage + 3) + (total - 1)) / 2));
698 items.push(<li key={`el-r`}><a href="#" className="pagination-link bp--page-num bp--ellipsis" data-page={mid} title={`Jump to page ${mid}`} onClick={(e) => { e.preventDefault(); setTxPage(mid); }}>...</a></li>);
699 ellipsisRight = true;
700 }
701 }
702 }
703 return items;
704 })()}
705 <li>
706 <a
707 href="#"
708 className={`pagination-next bp--next${txPage >= txMeta.pages ? ' disabled' : ''}`}
709 aria-disabled={txPage >= txMeta.pages}
710 onClick={(e) => { e.preventDefault(); if (txPage < txMeta.pages) setTxPage(p => p + 1); }}
711 >
712 <i className="bp-icon bp-caret-right"></i>
713 </a>
714 </li>
715 </ul>
716 </div>
717 )}
718 </div>
719 </div>
720 </div>
721 </div>
722 )}
723
724 {/* Subscriptions Tab (Pro only) */}
725 {subscriptionsShow && activeTab === 'subscriptions' && (
726 <SubscriptionsTab
727 proEnabled={proEnabled}
728 proAssets={proAssets}
729 subscriptions={subRows}
730 subPage={subPage}
731 subMeta={subMeta}
732 setSubPage={setSubPage}
733 attributes={attributes}
734 noItemsLabel={attributes.noItemsLabel}
735 />
736 )}
737 </div>
738 </div>
739 </div>
740 );
741 }
742