PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.1.18
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.1.18
2.4.3 2.4.4 2.4.2 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 All 60 releases
easy-invoice / templates / payments / list.php

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

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