PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.1
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / templates / payments / list.php

list.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.3.1, at templates/payments/list.php

1,049 lines 53.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Payments List Template
4 *
5 * @package Easy_Invoice
6 */
7
8 // Ensure we're in the correct namespace
9 namespace EasyInvoice\Templates\Payments;
10
11 // Exit if accessed directly
12 if (!defined('ABSPATH')) {
13 exit;
14 }
15
16 use EasyInvoice\Models\Payment;
17 use EasyInvoice\Models\Invoice;
18
19
20 // Extract data passed from controller
21 $payments = $payments ?? [];
22 $current_view = $current_view ?? 'all';
23 $status_filter = $status_filter ?? '';
24 $status_filters = $status_filters ?? [];
25 $trash_count = $trash_count ?? 0;
26 $stats = $stats ?? [];
27 $current_page = $current_page ?? 1;
28 $per_page = $per_page ?? 20;
29 $total_payments = $total_payments ?? 0;
30 $total_pages = $total_pages ?? 1;
31
32 // Ensure all required stats keys exist with default values
33 $stats = array_merge([
34 'total_payments' => 0,
35 'total_amount' => 0,
36 'completed_payments' => 0,
37 'pending_payments' => 0,
38 'failed_payments' => 0
39 ], $stats);
40
41 // Initialize amounts_by_currency array
42 $amounts_by_currency = [];
43 // Calculate stats from the payments array
44 foreach ($payments as $payment) {
45 $amount = floatval($payment->getAmount());
46 $status = $payment->getStatus();
47
48 $stats['total_amount'] += $amount;
49
50 switch ($status) {
51 case 'completed':
52 $stats['completed_payments']++;
53 break;
54 case 'pending':
55 $stats['pending_payments']++;
56 break;
57 case 'failed':
58 $stats['failed_payments']++;
59 break;
60 }
61
62 // Calculate currency breakdown
63 $payment_currency = $payment->getCurrency();
64
65 // If currency is empty or "global", get the actual currency that was used
66 if (empty($payment_currency) || $payment_currency === 'global') {
67 $actual_currency = get_post_meta($payment->getId(), '_currency', true);
68 $payment_currency = !empty($actual_currency) ? $actual_currency : get_option('easy_invoice_currency_code', 'USD');
69 }
70
71 // If currency is still "global", use the global setting
72 if ($payment_currency === 'global') {
73 $payment_currency = get_option('easy_invoice_currency_code', 'USD');
74 }
75
76 $payment_currency_symbol = $payment->getCurrencySymbol() ?: \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($payment_currency);
77
78 // Normalize currency code to uppercase for consistent grouping
79 $payment_currency = strtoupper($payment_currency);
80
81 // Group amounts by currency
82 if (!isset($amounts_by_currency[$payment_currency])) {
83 $amounts_by_currency[$payment_currency] = [
84 'amount' => 0,
85 'symbol' => $payment_currency_symbol
86 ];
87 }
88 $amounts_by_currency[$payment_currency]['amount'] += $amount;
89 }
90
91 // Check for bulk action notifications
92 $notification = false;
93
94 if (isset($_GET['bulk_trashed']) && $_GET['bulk_trashed'] > 0) {
95 $count = intval($_GET['bulk_trashed']);
96 easy_invoice_display_notification('success', sprintf(_n('%s payment moved to trash.', '%s payments moved to trash.', $count, 'easy-invoice'), $count));
97 $notification = true;
98 }
99
100 if (isset($_GET['bulk_restored']) && $_GET['bulk_restored'] > 0) {
101 $count = intval($_GET['bulk_restored']);
102 easy_invoice_display_notification('success', sprintf(_n('%s payment restored from trash.', '%s payments restored from trash.', $count, 'easy-invoice'), $count));
103 $notification = true;
104 }
105
106 if (isset($_GET['bulk_deleted']) && $_GET['bulk_deleted'] > 0) {
107 $count = intval($_GET['bulk_deleted']);
108 easy_invoice_display_notification('success', sprintf(_n('%s payment permanently deleted.', '%s payments permanently deleted.', $count, 'easy-invoice'), $count));
109 $notification = true;
110 }
111
112 if (isset($_GET['bulk_error'])) {
113 $error = sanitize_text_field($_GET['bulk_error']);
114 $error_message = 'An error occurred while processing the bulk action.';
115
116 if ($error === 'no_selection') {
117 $error_message = 'Please select at least one payment to perform this action.';
118 } elseif ($error === 'invalid_action') {
119 $error_message = 'Please select a valid bulk action.';
120 }
121
122 easy_invoice_display_notification('error', $error_message);
123 $notification = true;
124 }
125
126 ?>
127 <div class="p-8">
128 <div class="ei-app-page-header bg-white border-b border-gray-200 px-6" style="margin-left: -2rem; margin-top: -2rem; margin-right: -2rem; padding-left: 2rem; padding-right: 2rem;">
129 <div class="flex items-center justify-between">
130 <div>
131 <h1 class="text-2xl font-bold text-gray-900"><?php _e('Payments', 'easy-invoice'); ?></h1>
132 <p class="mt-1 text-sm text-gray-500"><?php _e('Manage all payment records', 'easy-invoice'); ?></p>
133 </div>
134 <div class="flex items-center space-x-3">
135 <?php
136 // "Export All" is part of the bulk_operations addon
137 // (Personal tier). See templates/invoices/listing.php
138 // for the canonical three-state gate pattern.
139 $ei_bulk_ops_loaded = \EasyInvoice\Addons\AddonManager::shouldLoad('bulk_operations');
140 ?>
141 <?php if ($ei_bulk_ops_loaded): ?>
142 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline;">
143 <input type="hidden" name="action" value="easy_invoice_export_all_payments" />
144 <input type="hidden" name="_wpnonce" value="<?php echo wp_create_nonce('easy_invoice_export_all_payments'); ?>" />
145 <button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
146 <svg class="w-4 h-4 mr-2 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
147 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
148 </svg>
149 <?php _e('Export All', 'easy-invoice'); ?>
150 </button>
151 </form>
152 <?php elseif (!easy_invoice_has_pro()): ?>
153 <button type="button" id="export-all-payments-btn" class="premium-export-btn inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200 relative">
154 <svg class="w-4 h-4 mr-2 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
155 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
156 </svg>
157 <?php _e('Export All', 'easy-invoice'); ?>
158 <svg class="ml-2 w-5 h-5 text-purple-600 crown-icon" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
159 <path d="M12 8L15 13.2L18 10.5L17.3 14H6.7L6 10.5L9 13.2L12 8M12 4L8.5 10L3 5L5 16H19L21 5L15.5 10L12 4Z"/>
160 </svg>
161 </button>
162 <script>
163 jQuery(document).ready(function($) {
164 $('#export-all-payments-btn').on('click', function(e) {
165 e.preventDefault();
166 if (typeof EasyInvoiceConfirmation !== 'undefined') {
167 EasyInvoiceConfirmation.showFeatureUpgrade('Export All Payments', 'Export all your payments to CSV, Excel, or PDF format for backup, analysis, or sharing with your team.');
168 } else {
169 EasyInvoiceToast.error('<?php echo esc_js(__('Exporting all payments is a premium feature. Upgrade to Easy Invoice Pro to unlock this feature.', 'easy-invoice')); ?>');
170 }
171 });
172 });
173 </script>
174 <?php endif; ?>
175
176 <a href="?page=easy-invoice-payment-new" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
177 <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
178 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
179 </svg>
180 <?php _e('Add New Payment', 'easy-invoice'); ?>
181 </a>
182 </div>
183 </div>
184 </div>
185
186 <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8 mt-8">
187 <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
188 <div class="flex items-center">
189 <div class="rounded-full bg-indigo-100 p-3 mr-4">
190 <svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
191 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
192 </svg>
193 </div>
194 <div>
195 <h3 class="text-sm font-medium text-gray-500">Total Payments</h3>
196 <p class="mt-1 text-2xl font-bold text-gray-900"><?php echo $stats['total_payments']; ?></p>
197 </div>
198 </div>
199 </div>
200 <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
201 <div class="flex items-center">
202 <div class="rounded-full bg-green-100 p-3 mr-4">
203 <svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
204 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
205 </svg>
206 </div>
207 <div>
208 <h3 class="text-sm font-medium text-gray-500">Total Amount</h3>
209 <?php if (!empty($amounts_by_currency)): ?>
210 <div class="mt-1">
211 <?php foreach ($amounts_by_currency as $currency => $data): ?>
212 <p class="text-lg font-bold text-gray-900">
213 <?php echo $data['symbol'] . number_format($data['amount'], 2); ?>
214 <span class="text-sm font-normal text-gray-500"><?php echo strtoupper($currency ?? 'USD'); ?></span>
215 </p>
216 <?php endforeach; ?>
217 </div>
218 <?php else: ?>
219 <p class="mt-1 text-lg font-bold text-gray-900"><?php echo get_option('easy_invoice_currency_symbol', '$') . '0.00'; ?></p>
220 <?php endif; ?>
221 </div>
222 </div>
223 </div>
224 <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
225 <div class="flex items-center">
226 <div class="rounded-full bg-green-100 p-3 mr-4">
227 <svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
228 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
229 </svg>
230 </div>
231 <div>
232 <h3 class="text-sm font-medium text-gray-500">Completed Payments</h3>
233 <p class="mt-1 text-2xl font-bold text-green-600"><?php echo $stats['completed_payments']; ?></p>
234 </div>
235 </div>
236 </div>
237 <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
238 <div class="flex items-center">
239 <div class="rounded-full bg-yellow-100 p-3 mr-4">
240 <svg class="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
241 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
242 </svg>
243 </div>
244 <div>
245 <h3 class="text-sm font-medium text-gray-500">Pending Payments</h3>
246 <p class="mt-1 text-2xl font-bold text-yellow-600"><?php echo $stats['pending_payments']; ?></p>
247 </div>
248 </div>
249 </div>
250 </div>
251
252 <?php if ($total_payments > 0): ?>
253 <div class="mb-4">
254 <div class="px-4 py-3 flex items-center justify-between">
255 <div class="flex items-center space-x-2">
256 <span class="text-sm text-gray-700">
257 Showing
258 <span class="font-medium"><?php echo number_format((($current_page - 1) * $per_page) + 1); ?></span>
259 to
260 <span class="font-medium"><?php echo number_format(min($current_page * $per_page, $total_payments)); ?></span>
261 of
262 <span class="font-medium"><?php echo number_format($total_payments); ?></span>
263 results
264 </span>
265 </div>
266 <?php if ($total_pages > 1): ?>
267 <div class="flex items-center space-x-2">
268 <span class="text-sm text-gray-500">Page <?php echo number_format($current_page); ?> of <?php echo number_format($total_pages); ?></span>
269 <?php
270 // Use WordPress's built-in pagination for top
271 $top_pagination_args = array(
272 'base' => add_query_arg('paged', '%#%'),
273 'format' => '',
274 'current' => $current_page,
275 'total' => $total_pages,
276 'prev_text' => '‹ Previous',
277 'next_text' => 'Next ›',
278 'type' => 'array',
279 'end_size' => 1,
280 'mid_size' => 2,
281 'add_args' => array(
282 'page' => 'easy-invoice-payments',
283 'view' => $current_view,
284 'status' => $status_filter
285 )
286 );
287
288 $top_pagination_links = paginate_links($top_pagination_args);
289
290 if ($top_pagination_links) {
291 echo '<div class="flex space-x-1">';
292 foreach ($top_pagination_links as $link) {
293 // Add null check to prevent passing null to easy_invoice_str_replace
294 if ($link === null) {
295 continue;
296 }
297
298 // Convert WordPress pagination to our styling
299 $link = easy_invoice_str_replace('page-numbers', 'px-3 py-2 text-sm font-medium border border-gray-300 rounded-md', $link);
300 $link = easy_invoice_str_replace('current', 'bg-indigo-50 border-indigo-500 text-indigo-600', $link);
301 $link = easy_invoice_str_replace('prev', 'bg-white text-gray-500 hover:bg-gray-50', $link);
302 $link = easy_invoice_str_replace('next', 'bg-white text-gray-500 hover:bg-gray-50', $link);
303 $link = easy_invoice_str_replace('dots', 'bg-white text-gray-700', $link);
304
305 // Add default styling for regular page numbers
306 if ($link && strpos($link, 'bg-indigo-50') === false && strpos($link, 'bg-white') === false) {
307 $link = easy_invoice_str_replace('border-gray-300', 'border-gray-300 bg-white text-gray-500 hover:bg-gray-50', $link);
308 }
309
310 echo $link;
311 }
312 echo '</div>';
313 }
314 ?>
315 </div>
316 <?php endif; ?>
317 </div>
318 </div>
319 <?php endif; ?>
320
321 <div class="bg-white rounded-lg shadow mb-6 overflow-hidden">
322 <div class="border-b border-gray-200 bg-gradient-to-r from-indigo-50 to-white">
323 <nav class="flex">
324 <?php
325 // View Tabs (All, Trash)
326 $views = array(
327 'all' => 'All (' . $stats['total_payments'] . ')',
328 'trash' => 'Trash (' . $trash_count . ')'
329 );
330
331 foreach ($views as $view => $label) {
332 $is_current = $current_view === $view;
333 $status_param = !empty($status_filter) && $view === 'all' ? '&status=' . $status_filter : '';
334 $class = $is_current
335 ? 'border-b-2 border-indigo-500 text-indigo-600 bg-white'
336 : 'border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300';
337 ?>
338 <a href="?page=easy-invoice-payments&view=<?php echo $view . $status_param; ?>"
339 class="flex items-center whitespace-nowrap py-4 px-6 font-medium text-sm transition-colors duration-200 <?php echo $class; ?>">
340 <?php echo $label; ?>
341 </a>
342 <?php } ?>
343 </nav>
344 </div>
345 </div>
346
347 <div class="bg-white shadow overflow-hidden sm:rounded-lg">
348 <div class="px-6 py-3 border-b border-gray-200 bg-gray-50">
349 <div class="flex items-center justify-between">
350 <div class="flex items-center space-x-4">
351 <form id="bulk-action-form" method="post" class="flex items-center gap-2">
352 <?php wp_nonce_field('easy_invoice_payment_bulk_action', 'easy_invoice_payment_bulk_nonce'); ?>
353 <input type="hidden" name="action" value="easy_invoice_payment_bulk_action">
354
355 <select name="bulk_action" class="block w-full px-3 py-2 border border-gray-300 rounded-md leading-5 bg-white text-gray-700 placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
356 <option value="">Bulk Actions</option>
357 <?php if ($current_view === 'trash'): ?>
358 <option value="restore">Restore</option>
359 <option value="delete">Delete Permanently</option>
360 <?php else: ?>
361 <option value="trash">Move to Trash</option>
362 <option value="delete">Delete Permanently</option>
363 <?php endif; ?>
364 </select>
365
366 <button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
367 Apply
368 </button>
369 </form>
370 </div>
371 <div class="text-sm text-gray-500">
372 <span id="selected-count">0</span> payments selected
373 </div>
374 </div>
375 </div>
376
377 <div class="ei-table-responsive">
378 <table class="min-w-full divide-y divide-gray-200 ei-payments-table">
379 <thead class="bg-gray-50">
380 <tr>
381 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
382 <input type="checkbox" id="select-all-payments" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
383 </th>
384 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="id">
385 <div class="flex items-center space-x-1">
386 <span>Payment ID</span>
387 <div class="sort-indicator">
388 <i class="fas fa-sort text-gray-400"></i>
389 </div>
390 </div>
391 </th>
392 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="invoice">
393 <div class="flex items-center space-x-1">
394 <span>Invoice</span>
395 <div class="sort-indicator">
396 <i class="fas fa-sort text-gray-400"></i>
397 </div>
398 </div>
399 </th>
400 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="amount">
401 <div class="flex items-center space-x-1">
402 <span>Amount</span>
403 <div class="sort-indicator">
404 <i class="fas fa-sort text-gray-400"></i>
405 </div>
406 </div>
407 </th>
408 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="method">
409 <div class="flex items-center space-x-1">
410 <span>Method</span>
411 <div class="sort-indicator">
412 <i class="fas fa-sort text-gray-400"></i>
413 </div>
414 </div>
415 </th>
416 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="status">
417 <div class="flex items-center space-x-1">
418 <span>Status</span>
419 <div class="sort-indicator">
420 <i class="fas fa-sort text-gray-400"></i>
421 </div>
422 </div>
423 </th>
424 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="date">
425 <div class="flex items-center space-x-1">
426 <span>Date</span>
427 <div class="sort-indicator">
428 <i class="fas fa-sort text-gray-400"></i>
429 </div>
430 </div>
431 </th>
432 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 hidden sm:table-cell" data-sort="type">
433 <div class="flex items-center space-x-1">
434 <span>Type</span>
435 <div class="sort-indicator">
436 <i class="fas fa-sort text-gray-400"></i>
437 </div>
438 </div>
439 </th>
440 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
441 Actions
442 </th>
443 </tr>
444 </thead>
445 <tbody class="bg-white divide-y divide-gray-200">
446 <?php if ($payments): ?>
447 <?php foreach ($payments as $payment): ?>
448 <?php
449 // $payment is already a Payment object, no need to create new one
450
451 // Get invoice information
452 $invoice_id = $payment->getInvoiceId();
453
454 // Temporarily show all payments regardless of invoice
455 ?>
456 <tr>
457 <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
458 <input type="checkbox" name="payment_ids[]" value="<?php echo $payment->getId(); ?>" form="bulk-action-form" class="payment-checkbox rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
459 </td>
460 <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
461 #<?php echo $payment->getId(); ?>
462 </td>
463 <td class="px-6 py-4 text-sm text-gray-500">
464 <?php if ($invoice_id): ?>
465 <?php
466 $invoice_post = get_post($invoice_id);
467 if ($invoice_post && $invoice_post->post_type === 'easy_invoice'):
468 $invoice = new \EasyInvoice\Models\Invoice($invoice_id);
469 ?>
470 <div>
471 <a href="<?php echo get_permalink($invoice->getId()); ?>" class="text-indigo-600 hover:text-indigo-900 font-medium" target="_blank">
472 <?php echo $invoice->getNumber(); ?>
473 </a>
474 <div class="text-xs text-gray-400 mt-1">
475 <?php echo esc_html($invoice->getTitle() ?: __('Untitled Invoice', 'easy-invoice')); ?>
476 </div>
477 </div>
478 <?php else: ?>
479 <span class="text-gray-400">
480 <?php _e('Invoice not found', 'easy-invoice'); ?>
481 <?php if (current_user_can('manage_options')): ?>
482 <br><small class="text-xs text-gray-500">ID: <?php echo $invoice_id; ?> (Type: <?php echo $invoice_post ? $invoice_post->post_type : 'null'; ?>)</small>
483 <?php endif; ?>
484 </span>
485 <?php endif; ?>
486 <?php else: ?>
487 <span class="text-gray-400">
488 <?php _e('No invoice ID', 'easy-invoice'); ?>
489 </span>
490 <?php endif; ?>
491 </td>
492 <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
493 <?php echo $payment->getCurrencySymbol() . number_format($payment->getAmount(), 2); ?>
494 </td>
495 <td class="px-6 py-4 text-sm text-gray-500">
496 <?php
497 // Get payment method label dynamically from registered gateways
498 $payment_method = $payment->getPaymentMethod();
499 $gateway_manager = \EasyInvoice\EasyInvoice::getInstance()->getGatewayManager();
500 $gateways = $gateway_manager->getGateways();
501
502 // Try to get the gateway title from registered gateways
503 $method_label = ucfirst(easy_invoice_str_replace('_', ' ', $payment_method)); // Default fallback
504
505 foreach ($gateways as $gateway) {
506 if ($gateway->getName() === $payment_method) {
507 $method_label = $gateway_manager->getGatewayDisplayName($gateway->getName());
508 break;
509 }
510 }
511
512 // Special case for manual payments
513 if ($payment_method === 'manual') {
514 $method_label = __('Manual', 'easy-invoice');
515 }
516
517 echo esc_html($method_label);
518 ?>
519 </td>
520 <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
521 <?php
522 $status = $payment->getStatus();
523 $status_class = '';
524 $status_text = '';
525
526 switch ($status) {
527 case 'completed':
528 $status_class = 'bg-green-100 text-green-800';
529 $status_text = __('Completed', 'easy-invoice');
530 break;
531 case 'pending':
532 $status_class = 'bg-yellow-100 text-yellow-800';
533 $status_text = __('Pending', 'easy-invoice');
534 break;
535 case 'failed':
536 $status_class = 'bg-red-100 text-red-800';
537 $status_text = __('Failed', 'easy-invoice');
538 break;
539 default:
540 $status_class = 'bg-gray-100 text-gray-800';
541 $status_text = ucfirst($status);
542 }
543 ?>
544 <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <?php echo $status_class; ?>">
545 <?php echo $status_text; ?>
546 </span>
547 </td>
548 <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
549 <?php echo $payment->getPaymentDate() ? date('M j, Y', strtotime($payment->getPaymentDate())) : date('M j, Y', strtotime($payment->getPost()->post_date)); ?>
550 </td>
551 <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 hidden sm:table-cell">
552 <?php echo ucfirst($payment->getPaymentType() ?: 'one-time'); ?>
553 </td>
554 <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
555 <div class="flex items-center space-x-2">
556 <a href="?page=easy-invoice-payments&action=view&id=<?php echo $payment->getId(); ?>" class="text-indigo-600 hover:text-indigo-900">
557 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
558 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
559 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
560 </svg>
561 </a>
562 <a href="?page=easy-invoice-payments&action=edit&id=<?php echo $payment->getId(); ?>" class="text-blue-600 hover:text-blue-900">
563 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
564 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
565 </svg>
566 </a>
567 <?php if (in_array($status, ['pending', 'pending-bank', 'pending-cheque'])): ?>
568 <button type="button" class="approve-payment-btn text-green-600 hover:text-green-900"
569 data-payment-id="<?php echo $payment->getId(); ?>"
570 data-invoice-id="<?php echo $payment->getInvoiceId(); ?>"
571 data-nonce="<?php echo wp_create_nonce('easy_invoice_approve_payment'); ?>">
572 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
573 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
574 </svg>
575 </button>
576 <?php endif; ?>
577
578 <?php
579 // Hook for additional payment actions (used by Pro plugin for receipts)
580 do_action('easy_invoice_payment_row_actions', $payment, $status);
581 ?>
582 </div>
583 </td>
584 </tr>
585 <?php endforeach; ?>
586 <?php else : ?>
587 <tr>
588 <td colspan="9" class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-center">
589 <?php _e('No payments found.', 'easy-invoice'); ?>
590 </td>
591 </tr>
592 <?php endif; ?>
593 </tbody>
594 </table>
595 </div>
596 </div>
597
598 <?php if ($total_payments > 0): ?>
599 <div class="mt-4">
600 <div class="px-4 py-3 flex items-center justify-between">
601 <div class="flex items-center space-x-2">
602 <span class="text-sm text-gray-700">
603 Showing
604 <span class="font-medium"><?php echo number_format((($current_page - 1) * $per_page) + 1); ?></span>
605 to
606 <span class="font-medium"><?php echo number_format(min($current_page * $per_page, $total_payments)); ?></span>
607 of
608 <span class="font-medium"><?php echo number_format($total_payments); ?></span>
609 results
610 </span>
611 </div>
612 <?php if ($total_pages > 1): ?>
613 <div class="flex items-center space-x-2">
614 <span class="text-sm text-gray-500">Page <?php echo number_format($current_page); ?> of <?php echo number_format($total_pages); ?></span>
615 <?php
616 // Use WordPress's built-in pagination for bottom
617 $bottom_pagination_args = array(
618 'base' => add_query_arg('paged', '%#%'),
619 'format' => '',
620 'current' => $current_page,
621 'total' => $total_pages,
622 'prev_text' => '‹ Previous',
623 'next_text' => 'Next ›',
624 'type' => 'array',
625 'end_size' => 1,
626 'mid_size' => 2,
627 'add_args' => array(
628 'page' => 'easy-invoice-payments',
629 'view' => $current_view,
630 'status' => $status_filter
631 )
632 );
633
634 $bottom_pagination_links = paginate_links($bottom_pagination_args);
635
636 if ($bottom_pagination_links) {
637 echo '<div class="flex space-x-1">';
638 foreach ($bottom_pagination_links as $link) {
639 // Add null check to prevent passing null to easy_invoice_str_replace
640 if ($link === null) {
641 continue;
642 }
643
644 // Convert WordPress pagination to our styling
645 $link = easy_invoice_str_replace('page-numbers', 'px-3 py-2 text-sm font-medium border border-gray-300 rounded-md', $link);
646 $link = easy_invoice_str_replace('current', 'bg-indigo-50 border-indigo-500 text-indigo-600', $link);
647 $link = easy_invoice_str_replace('prev', 'bg-white text-gray-500 hover:bg-gray-50', $link);
648 $link = easy_invoice_str_replace('next', 'bg-white text-gray-500 hover:bg-gray-50', $link);
649 $link = easy_invoice_str_replace('dots', 'bg-white text-gray-700', $link);
650
651 // Add default styling for regular page numbers
652 if ($link && strpos($link, 'bg-indigo-50') === false && strpos($link, 'bg-white') === false) {
653 $link = easy_invoice_str_replace('border-gray-300', 'border-gray-300 bg-white text-gray-500 hover:bg-gray-50', $link);
654 }
655
656 echo $link;
657 }
658 echo '</div>';
659 }
660 ?>
661 </div>
662 <?php endif; ?>
663 </div>
664 </div>
665 <?php endif; ?>
666 </div>
667
668 <div id="payment-approval-modal" class="fixed inset-0 z-50 overflow-y-auto hidden" aria-labelledby="modal-title" role="dialog" aria-modal="true">
669 <div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
670 <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
671
672 <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
673 <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
674 <div class="sm:flex sm:items-start">
675 <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-green-100 sm:mx-0 sm:h-10 sm:w-10">
676 <svg class="h-6 w-6 text-green-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
677 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
678 </svg>
679 </div>
680 <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
681 <h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">
682 <?php _e('Approve Payment', 'easy-invoice'); ?>
683 </h3>
684 <div class="mt-2">
685 <p class="text-sm text-gray-500">
686 <?php _e('You are about to approve this payment. This will mark the invoice as paid and update the payment records.', 'easy-invoice'); ?>
687 </p>
688 <div class="mt-4">
689 <label for="approval-notes" class="block text-sm font-medium text-gray-700">
690 <?php _e('Notes (optional)', 'easy-invoice'); ?>
691 </label>
692 <textarea id="approval-notes" name="notes" rows="3" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="<?php _e('Add any notes about this payment approval...', 'easy-invoice'); ?>"></textarea>
693 </div>
694 </div>
695 </div>
696 </div>
697 </div>
698 <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
699 <button type="button" id="confirm-approve-btn" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-green-600 text-base font-medium text-white hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 sm:ml-3 sm:w-auto sm:text-sm">
700 <?php _e('Approve Payment', 'easy-invoice'); ?>
701 </button>
702 <button type="button" id="cancel-approve-btn" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
703 <?php _e('Cancel', 'easy-invoice'); ?>
704 </button>
705 </div>
706 </div>
707 </div>
708 </div>
709
710 <div id="payment-approve-notification" class="fixed top-0 right-0 m-4 hidden">
711 </div>
712
713 <style>
714 /* Add any additional custom styles here */
715 .ei-table-responsive{
716 width: 100%;
717 max-width: 100%;
718 overflow-x: auto;
719 -webkit-overflow-scrolling: touch;
720 }
721 .ei-payments-table{
722 width: 100%;
723 min-width: 900px;
724 }
725 @media (max-width: 640px) {
726 .ei-payments-table th,
727 .ei-payments-table td{
728 padding-left: 0.75rem !important;
729 padding-right: 0.75rem !important;
730 }
731 }
732 .payment-status {
733 display: inline-block;
734 padding: 3px 8px;
735 border-radius: 3px;
736 font-size: 12px;
737 font-weight: 600;
738 }
739 .status-completed {
740 background-color: #dff0d8;
741 color: #3c763d;
742 }
743 .status-pending {
744 background-color: #fcf8e3;
745 color: #8a6d3b;
746 }
747 .status-failed {
748 background-color: #f2dede;
749 color: #a94442;
750 }
751 </style>
752
753 <script>
754 // Define ajaxurl for AJAX requests
755 var ajaxurl = '<?php echo admin_url('admin-ajax.php'); ?>';
756
757 jQuery(document).ready(function($) {
758 // Variables to store the current payment being approved
759 let currentInvoiceId = null;
760 let currentPaymentId = null;
761 let currentNonce = null;
762
763 // Handle Approve button click
764 $('.approve-payment-btn').on('click', function(e) {
765 e.preventDefault();
766
767 // Store the data for the current payment
768 currentInvoiceId = $(this).data('invoice-id');
769 currentPaymentId = $(this).data('payment-id');
770 currentNonce = $(this).data('nonce');
771
772 // Show the approval modal
773 $('#payment-approval-modal').removeClass('hidden');
774 });
775
776 // Handle Cancel button in modal
777 $('#cancel-approve-btn').on('click', function() {
778 // Hide the modal and reset the form
779 $('#payment-approval-modal').addClass('hidden');
780 $('#approval-notes').val('');
781 });
782
783 // Handle Confirm Approve button in modal
784 $('#confirm-approve-btn').on('click', function() {
785 // Get the notes from the textarea
786 const notes = $('#approval-notes').val();
787
788 // Disable the button to prevent double clicks
789 $(this).prop('disabled', true).text('Processing...');
790
791 // Make AJAX request to approve the payment
792 $.ajax({
793 url: ajaxurl,
794 type: 'POST',
795 data: {
796 action: 'easy_invoice_approve_payment',
797 invoice_id: currentInvoiceId,
798 nonce: currentNonce,
799 notes: notes
800 },
801 success: function(response) {
802 // Hide the modal
803 $('#payment-approval-modal').addClass('hidden');
804
805 if (response.success) {
806 // Show success toast
807 if (typeof EasyInvoiceToast !== 'undefined') {
808 EasyInvoiceToast.success('Payment approved successfully.');
809 }
810
811 // Refresh the page after a short delay
812 setTimeout(function() {
813 location.reload();
814 }, 1500);
815 } else {
816 // Show error toast
817 if (typeof EasyInvoiceToast !== 'undefined') {
818 EasyInvoiceToast.error('Failed to approve payment.');
819 }
820
821 // Re-enable the button
822 $('#confirm-approve-btn').prop('disabled', false).text('Approve Payment');
823 }
824 },
825 error: function() {
826 // Hide the modal
827 $('#payment-approval-modal').addClass('hidden');
828
829 // Show error toast
830 if (typeof EasyInvoiceToast !== 'undefined') {
831 EasyInvoiceToast.error('Server error occurred. Please try again.');
832 }
833
834 // Re-enable the button
835 $('#confirm-approve-btn').prop('disabled', false).text('Approve Payment');
836 }
837 });
838 });
839
840
841
842 // Close the modal if clicking outside of it
843 $(window).on('click', function(e) {
844 if ($(e.target).is('#payment-approval-modal')) {
845 $('#payment-approval-modal').addClass('hidden');
846 $('#approval-notes').val('');
847 }
848 });
849
850 // Close the modal with Escape key
851 $(document).on('keydown', function(e) {
852 if (e.key === "Escape" && !$('#payment-approval-modal').hasClass('hidden')) {
853 $('#payment-approval-modal').addClass('hidden');
854 $('#approval-notes').val('');
855 }
856 });
857
858 // Dismiss notification buttons
859 $('.notification-dismiss').on('click', function() {
860 $(this).closest('.rounded-md').remove();
861 });
862
863 // Payment Table Sorting and Filtering Functionality
864 let currentSort = { column: '', direction: '' };
865
866 // Initialize sorting and filtering
867 function initSortingAndFiltering() {
868 // Add click handlers to sortable headers
869 $('th[data-sort]').on('click', function() {
870 const column = $(this).data('sort');
871 const currentDirection = currentSort.column === column ? currentSort.direction : 'desc';
872 const newDirection = currentDirection === 'asc' ? 'desc' : 'asc';
873
874 currentSort = { column: column, direction: newDirection };
875 updateSortIndicator(column, newDirection);
876 sortTable(column, newDirection);
877 });
878 }
879
880 // Update sort indicator
881 function updateSortIndicator(activeColumn, direction) {
882 // Reset all indicators
883 $('.sort-indicator i').removeClass('fa-sort-up fa-sort-down').addClass('fa-sort text-gray-400');
884
885 // Set active indicator
886 $(`th[data-sort="${activeColumn}"] .sort-indicator i`)
887 .removeClass('fa-sort text-gray-400')
888 .addClass(direction === 'asc' ? 'fa-sort-up text-indigo-600' : 'fa-sort-down text-indigo-600');
889 }
890
891 // Sort table function
892 function sortTable(column, direction) {
893 const tbody = $('tbody');
894 const rows = tbody.find('tr').toArray();
895
896 rows.sort(function(a, b) {
897 let aValue, bValue;
898
899 switch(column) {
900 case 'id':
901 aValue = parseInt($(a).find('td:first-child').text().replace('#', ''));
902 bValue = parseInt($(b).find('td:first-child').text().replace('#', ''));
903 break;
904 case 'invoice':
905 aValue = $(a).find('td:nth-child(2) a').text().toLowerCase();
906 bValue = $(b).find('td:nth-child(2) a').text().toLowerCase();
907 break;
908 case 'amount':
909 aValue = parseFloat($(a).find('td:nth-child(3)').text().replace(/[^0-9.-]+/g, ''));
910 bValue = parseFloat($(b).find('td:nth-child(3)').text().replace(/[^0-9.-]+/g, ''));
911 break;
912 case 'method':
913 aValue = $(a).find('td:nth-child(4)').text().toLowerCase();
914 bValue = $(b).find('td:nth-child(4)').text().toLowerCase();
915 break;
916 case 'status':
917 aValue = $(a).find('td:nth-child(5) span').text().toLowerCase();
918 bValue = $(b).find('td:nth-child(5) span').text().toLowerCase();
919 break;
920 case 'date':
921 aValue = new Date($(a).find('td:nth-child(6)').text());
922 bValue = new Date($(b).find('td:nth-child(6)').text());
923 break;
924 case 'type':
925 aValue = $(a).find('td:nth-child(7)').text().toLowerCase();
926 bValue = $(b).find('td:nth-child(7)').text().toLowerCase();
927 break;
928 default:
929 return 0;
930 }
931
932 if (direction === 'asc') {
933 return aValue > bValue ? 1 : -1;
934 } else {
935 return aValue < bValue ? 1 : -1;
936 }
937 });
938
939 // Reorder rows with animation
940 tbody.find('tr').fadeOut(200, function() {
941 tbody.empty();
942 $.each(rows, function(index, row) {
943 tbody.append(row);
944 });
945 tbody.find('tr').fadeIn(200);
946 });
947 }
948
949 // Initialize sorting and filtering
950 initSortingAndFiltering();
951
952 // Payment Delete Functionality
953 let selectedPayments = [];
954
955 // Handle select all checkbox
956 $('#select-all-payments').on('change', function() {
957 const isChecked = $(this).is(':checked');
958 $('.payment-checkbox').prop('checked', isChecked);
959
960 if (isChecked) {
961 selectedPayments = $('.payment-checkbox').map(function() {
962 return $(this).val();
963 }).get();
964 } else {
965 selectedPayments = [];
966 }
967
968 updateBulkActionButton();
969 });
970
971 // Handle individual checkboxes
972 $(document).on('change', '.payment-checkbox', function() {
973 const paymentId = $(this).val();
974 const isChecked = $(this).is(':checked');
975
976 if (isChecked) {
977 if (!selectedPayments.includes(paymentId)) {
978 selectedPayments.push(paymentId);
979 }
980 } else {
981 selectedPayments = selectedPayments.filter(id => id !== paymentId);
982 }
983
984 // Update select all checkbox
985 const totalCheckboxes = $('.payment-checkbox').length;
986 const checkedCheckboxes = $('.payment-checkbox:checked').length;
987
988 if (checkedCheckboxes === 0) {
989 $('#select-all-payments').prop('checked', false).prop('indeterminate', false);
990 } else if (checkedCheckboxes === totalCheckboxes) {
991 $('#select-all-payments').prop('checked', true).prop('indeterminate', false);
992 } else {
993 $('#select-all-payments').prop('checked', false).prop('indeterminate', true);
994 }
995
996 updateBulkActionButton();
997 });
998
999 // Update bulk action button state
1000 function updateBulkActionButton() {
1001 const button = $('#bulk-action-form button[type="submit"]');
1002 const countSpan = $('#selected-count');
1003
1004 if (selectedPayments.length > 0) {
1005 button.prop('disabled', false);
1006 countSpan.text(selectedPayments.length);
1007 } else {
1008 button.prop('disabled', true);
1009 countSpan.text('0');
1010 }
1011 }
1012
1013 // Form submission validation for bulk actions
1014 $("#bulk-action-form").on("submit", function(e) {
1015 e.preventDefault();
1016
1017 var bulkAction = $("select[name='bulk_action']").val();
1018 if (!bulkAction) {
1019 EasyInvoiceToast.error('Please select a bulk action.');
1020 return;
1021 }
1022
1023 if (selectedPayments.length === 0) {
1024 EasyInvoiceToast.error('Please select at least one payment.');
1025 return;
1026 }
1027
1028 // Confirm bulk actions
1029 if (bulkAction === 'trash') {
1030 EasyInvoiceConfirmation.confirmAction('trash', selectedPayments.length + ' payment(s)', function() {
1031 $("#bulk-action-form")[0].submit();
1032 });
1033 return;
1034 } else if (bulkAction === 'delete') {
1035 EasyInvoiceConfirmation.confirmAction('delete', selectedPayments.length + ' payment(s)', function() {
1036 $("#bulk-action-form")[0].submit();
1037 });
1038 return;
1039 } else if (bulkAction === 'restore') {
1040 EasyInvoiceConfirmation.confirmAction('restore', selectedPayments.length + ' payment(s)', function() {
1041 $("#bulk-action-form")[0].submit();
1042 });
1043 return;
1044 } else {
1045 $("#bulk-action-form")[0].submit();
1046 }
1047 });
1048 });
1049 </script>