PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.0
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.0
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 / quotes / listing.php

listing.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.0, at templates/quotes/listing.php

1,627 lines 94.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Quote Listing Template
4 *
5 * @package EasyInvoice
6 * @since 1.0.0
7 */
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12 // Initialize variables
13 $quotes = $quotes ?? [];
14 $stats = $stats ?? [
15 'total' => 0,
16 'available' => 0,
17 'draft' => 0,
18 'sent' => 0,
19 'accepted' => 0,
20 'declined' => 0,
21 'cancelled' => 0,
22 'expired' => 0
23 ];
24
25 // Get current view and status filter
26 $view = isset($_GET['view']) ? sanitize_text_field($_GET['view']) : 'all';
27 $status_filter = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '';
28
29 // Get client filter (from template data, falls back to GET)
30 $client_filter = isset($client_filter) ? (int) $client_filter : (isset($_GET['client_id']) ? absint($_GET['client_id']) : 0);
31 $clients_list = isset($clients_list) && is_array($clients_list) ? $clients_list : [];
32
33 // Define status filters
34 $status_filters = array(
35 'available' => 'Available',
36 'draft' => 'Draft',
37 'sent' => 'Sent',
38 'accepted' => 'Accepted',
39 'declined' => 'Declined',
40 'cancelled' => 'Trash',
41 'expired' => 'Expired'
42 );
43
44 // Extract quote data for statistics
45 $formatted_quotes = array();
46 foreach ($quotes as $quote) {
47 // For Quote model objects
48 if (is_object($quote)) {
49 $formatted_quotes[] = array(
50 'id' => $quote->getId(),
51 'amount' => $quote->getTotal(),
52 'status' => $quote->getStatus(),
53 );
54 } else {
55 // For already formatted quote arrays
56 $formatted_quotes[] = array(
57 'id' => $quote['id'],
58 'amount' => isset($quote['total_amount']) ? $quote['total_amount'] : $quote['amount'],
59 'status' => $quote['status'],
60 );
61 }
62 }
63
64 // Store-wide figures for the header cards. Counting the rows on screen gave
65 // "Total Quotes 20" on a 10,000-quote store; the controller passes the real
66 // per-status counts, and the value by currency is one SQL pass over the
67 // stored totals (accepted + sent + draft, i.e. quotes still in play or won).
68 $currency_totals = [];
69 $total_quotes = (int) ($draft_count ?? 0) + (int) ($available_count ?? 0) + (int) ($sent_count ?? 0) + (int) ($accepted_count ?? 0) + (int) ($declined_count ?? 0) + (int) ($expired_count ?? 0) + (int) ($cancelled_count ?? 0);
70
71 foreach (\EasyInvoice\Services\QuoteTotals::valueByCurrency() as $currency_code => $row) {
72 $currency_totals[$currency_code] = [
73 'total' => (float) $row['amount'],
74 'quotes' => (int) $row['count'],
75 'quote_object' => null,
76 'currency' => $currency_code,
77 ];
78 }
79
80 $enhanced_stats = [
81 'total_quotes' => $total_quotes,
82 'currency_totals' => $currency_totals,
83 'accepted_quotes' => (int) ($accepted_count ?? 0),
84 'pending_quotes' => (int) ($sent_count ?? 0) + (int) ($draft_count ?? 0) + (int) ($available_count ?? 0),
85 'rejected_quotes' => (int) ($declined_count ?? 0),
86 ];
87
88 ?>
89 <div class="p-8">
90 <div class="ei-app-page-header bg-white border-b border-gray-200 px-6" style="margin-left: -2rem; margin-top: -2rem; padding-left:2rem; padding-right:2rem; margin-right: -2rem;">
91 <div class="flex items-center justify-between">
92 <div>
93 <h1 class="text-2xl font-bold text-gray-900"><?php esc_html_e('Quotes', 'easy-invoice'); ?></h1>
94 <p class="mt-1 text-sm text-gray-500"><?php esc_html_e('Manage and track your quotes', 'easy-invoice'); ?></p>
95 </div>
96 <div class="flex items-center space-x-3">
97 <?php if (isset($_GET['rewrite_flushed'])): ?>
98 <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-2 rounded-md text-sm">
99 <?php esc_html_e('Rewrite rules flushed successfully!', 'easy-invoice'); ?>
100 </div>
101 <?php endif; ?>
102
103 <?php if (isset($_GET['post_types_registered'])): ?>
104 <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-2 rounded-md text-sm">
105 <?php esc_html_e('Post types registered successfully!', 'easy-invoice'); ?>
106 </div>
107 <?php endif; ?>
108
109 <?php
110 // "Flush Rewrite Rules" is a troubleshooting action, not something a
111 // user does as part of their job — but it rendered unconditionally as
112 // a prominent button right beside the primary "Create New Quote" CTA
113 // on every admin's screen. It is now hidden by default and shown when
114 // WP_DEBUG is on, or when a site opts in via the filter.
115 //
116 // Kept rather than deleted because it is the manual escape hatch if
117 // quote permalinks ever 404.
118 $ei_show_maintenance_tools = apply_filters(
119 'easy_invoice_show_maintenance_tools',
120 defined('WP_DEBUG') && WP_DEBUG
121 );
122 if ($ei_show_maintenance_tools) : ?>
123 <a href="<?php echo esc_url(wp_nonce_url(admin_url('admin-post.php?action=flush_easy_invoice_rewrite_rules'), 'flush_easy_invoice_rewrite_rules')); ?>"
124 class="bg-yellow-100 text-yellow-700 hover:bg-yellow-200 px-4 py-2 rounded-md text-sm font-medium transition-colors duration-200">
125 <?php esc_html_e('Flush Rewrite Rules', 'easy-invoice'); ?>
126 </a>
127 <?php endif; ?>
128
129 <?php
130 // "Export All" is part of the bulk_operations addon
131 // (Personal tier). See templates/invoices/listing.php
132 // for the canonical three-state gate pattern.
133 $ei_bulk_ops_loaded = \EasyInvoice\Addons\AddonManager::shouldLoad('bulk_operations');
134 ?>
135 <?php if ($ei_bulk_ops_loaded): ?>
136 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline;">
137 <input type="hidden" name="action" value="easy_invoice_export_all_quotes" />
138 <input type="hidden" name="_wpnonce" value="<?php echo esc_attr(wp_create_nonce('easy_invoice_export_all_quotes')); ?>" />
139 <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">
140 <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">
141 <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>
142 </svg>
143 <?php esc_html_e('Export All', 'easy-invoice'); ?>
144 </button>
145 </form>
146 <?php elseif (!easy_invoice_has_pro()): ?>
147 <button type="button" id="export-all-quotes-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">
148 <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">
149 <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>
150 </svg>
151 <?php esc_html_e('Export All', 'easy-invoice'); ?>
152 <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">
153 <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"/>
154 </svg>
155 </button>
156 <script>
157 jQuery(document).ready(function($) {
158 $('#export-all-quotes-btn').on('click', function(e) {
159 e.preventDefault();
160 if (typeof EasyInvoiceConfirmation !== 'undefined') {
161 EasyInvoiceConfirmation.showFeatureUpgrade('Export All Quotes', 'Export all your quotes to CSV, Excel, or PDF format for backup, analysis, or sharing with your team.');
162 } else {
163 alert('<?php echo esc_js(__('Exporting all quotes is a premium feature. Upgrade to Easy Invoice Pro to unlock this feature.', 'easy-invoice')); ?>');
164 }
165 });
166 });
167 </script>
168 <?php endif; ?>
169 <?php if (easy_invoice_user_can('ei_create_quote')) : ?>
170 <button type="button" id="create-quote-btn" 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">
171 <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
172 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
173 </svg>
174 <?php esc_html_e('Create New Quote', 'easy-invoice'); ?>
175 </button>
176 <?php endif; ?>
177 </div>
178 </div>
179 </div>
180
181 <div id="new-quote-modal" class="fixed inset-0 bg-gray-600 bg-opacity-75 overflow-y-auto h-full w-full z-50 hidden">
182 <div class="relative mx-auto p-6 border w-96 shadow-xl rounded-lg bg-white my-20">
183 <div class="flex justify-between items-center mb-6">
184 <h3 class="text-xl font-semibold text-gray-900"><?php esc_html_e('Create New Quote', 'easy-invoice'); ?></h3>
185 <button type="button" id="close-new-quote-modal" class="text-gray-400 hover:text-gray-600 transition-colors duration-200">
186 <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
187 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
188 </svg>
189 </button>
190 </div>
191
192 <form id="new-quote-form" class="space-y-6">
193 <div>
194 <label for="new_quote_title" class="block text-sm font-medium text-gray-700 mb-2">Quote Title *</label>
195 <input type="text"
196 name="new_quote_title"
197 id="new_quote_title"
198 required
199 placeholder="Enter quote title..."
200 class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-3 px-4 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm transition-colors duration-200">
201 <p class="mt-2 text-sm text-gray-500"><?php esc_html_e('This will be the title of your quote', 'easy-invoice'); ?></p>
202 </div>
203
204 <div class="flex justify-end space-x-3 pt-4">
205 <button type="button" id="cancel-new-quote"
206 class="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium 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">
207 Cancel
208 </button>
209 <button type="submit"
210 class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200">
211 Create Quote
212 </button>
213 </div>
214 </form>
215
216 <div id="new-quote-message" class="mt-4 hidden"></div>
217 </div>
218 </div>
219
220 <div id="logs-modal" class="fixed z-50 inset-0 overflow-y-auto hidden">
221 <div class="flex items-center justify-center min-h-screen px-4">
222 <div class="fixed inset-0 transition-opacity" aria-hidden="true">
223 <div class="absolute inset-0 bg-gray-500 opacity-75"></div>
224 </div>
225 <div class="bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:max-w-2xl w-full z-50">
226 <div class="px-6 py-4 border-b border-gray-200">
227 <div class="flex justify-between items-center">
228 <h3 class="text-lg font-medium text-gray-900"><?php esc_html_e('Quote Activity Logs', 'easy-invoice'); ?></h3>
229 <button type="button" id="close-logs-modal" class="text-gray-400 hover:text-gray-500 focus:outline-none">
230 <span class="sr-only"><?php esc_html_e('Close', 'easy-invoice'); ?></span>
231 <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
232 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
233 </svg>
234 </button>
235 </div>
236 </div>
237 <div class="px-6 py-4 modal-body">
238 </div>
239 </div>
240 </div>
241 </div>
242
243 <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8 mt-8">
244 <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
245 <div class="flex items-center">
246 <div class="rounded-full bg-indigo-100 p-3 mr-4">
247 <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">
248 <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>
249 </svg>
250 </div>
251 <div>
252 <h3 class="text-sm font-medium text-gray-500"><?php esc_html_e('Total Quotes', 'easy-invoice'); ?></h3>
253 <p class="mt-1 text-2xl font-bold text-gray-900"><?php echo esc_html($enhanced_stats['total_quotes']); ?></p>
254 </div>
255 </div>
256 </div>
257 <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
258 <div class="flex items-center">
259 <div class="rounded-full bg-green-100 p-3 mr-4">
260 <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">
261 <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>
262 </svg>
263 </div>
264 <div>
265 <h3 class="text-sm font-medium text-gray-500"><?php esc_html_e('Total Value', 'easy-invoice'); ?></h3>
266 <?php if (empty($enhanced_stats['currency_totals'])): ?>
267 <p class="mt-1 text-2xl font-bold text-gray-900">$0.00</p>
268 <?php else: ?>
269 <div class="mt-1 space-y-1">
270 <?php foreach ($enhanced_stats['currency_totals'] as $currency_code => $currency_data): ?>
271 <?php
272 $formatted_amount = easy_invoice_format_money((float) $currency_data['total'], null, (string) $currency_code);
273 ?>
274 <div class="flex items-center justify-between">
275 <span class="text-lg font-bold text-gray-900"><?php echo esc_html($formatted_amount); ?></span>
276 <span class="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded"><?php echo esc_html($currency_data['quotes']); ?> quote<?php echo $currency_data['quotes'] > 1 ? 's' : ''; ?></span>
277 </div>
278 <?php endforeach; ?>
279 </div>
280 <?php endif; ?>
281 </div>
282 </div>
283 </div>
284 <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
285 <div class="flex items-center">
286 <div class="rounded-full bg-green-100 p-3 mr-4">
287 <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">
288 <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>
289 </svg>
290 </div>
291 <div>
292 <h3 class="text-sm font-medium text-gray-500"><?php esc_html_e('Accepted Quotes', 'easy-invoice'); ?></h3>
293 <p class="mt-1 text-2xl font-bold text-green-600"><?php echo esc_html($enhanced_stats['accepted_quotes']); ?></p>
294 </div>
295 </div>
296 </div>
297 <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
298 <div class="flex items-center">
299 <div class="rounded-full bg-yellow-100 p-3 mr-4">
300 <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">
301 <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>
302 </svg>
303 </div>
304 <div>
305 <h3 class="text-sm font-medium text-gray-500"><?php esc_html_e('Pending Quotes', 'easy-invoice'); ?></h3>
306 <p class="mt-1 text-2xl font-bold text-yellow-600"><?php echo esc_html($enhanced_stats['pending_quotes']); ?></p>
307 </div>
308 </div>
309 </div>
310 </div>
311
312 <?php if (isset($pagination) && $pagination['total_pages'] > 1): ?>
313 <div class="mb-4">
314 <div class="px-4 py-3 flex items-center justify-between">
315 <div class="flex items-center space-x-2">
316 <span class="text-sm text-gray-700">
317 Showing
318 <span class="font-medium"><?php echo (int) ((($pagination['current_page'] - 1) * $pagination['per_page']) + 1); ?></span>
319 to
320 <span class="font-medium"><?php echo (int) min($pagination['current_page'] * $pagination['per_page'], $pagination['total_quotes']); ?></span>
321 of
322 <span class="font-medium"><?php echo esc_html($pagination['total_quotes']); ?></span>
323 results
324 </span>
325 </div>
326 <div class="flex items-center space-x-2">
327 <span class="text-sm text-gray-500">Page <?php echo esc_html($pagination['current_page']); ?> of <?php echo esc_html($pagination['total_pages']); ?></span>
328 <div class="flex space-x-1">
329 <?php
330 $prev_page = $pagination['current_page'] > 1 ? $pagination['current_page'] - 1 : 1;
331 $next_page = $pagination['current_page'] < $pagination['total_pages'] ? $pagination['current_page'] + 1 : $pagination['total_pages'];
332 $current_url = add_query_arg($_GET, admin_url('admin.php'));
333 $current_url = remove_query_arg('paged', $current_url);
334 $start_page = max(1, $pagination['current_page'] - 2);
335 $end_page = min($pagination['total_pages'], $pagination['current_page'] + 2);
336 if ($pagination['current_page'] > 1): ?>
337 <a class="bg-white text-gray-500 hover:bg-gray-50 px-3 py-2 text-sm font-medium border border-gray-300 rounded-md" href="<?php echo esc_url(add_query_arg('paged', $prev_page, $current_url)); ?>"> Previous</a>
338 <?php endif;
339 for ($i = $start_page; $i <= $end_page; $i++):
340 if ($i == $pagination['current_page']): ?>
341 <span class="px-3 py-2 text-sm font-medium border border-gray-300 rounded-md bg-indigo-50 border-indigo-500 text-indigo-600"><?php echo esc_html($i); ?></span>
342 <?php else: ?>
343 <a class="px-3 py-2 text-sm font-medium border border-gray-300 bg-white text-gray-500 hover:bg-gray-50 rounded-md" href="<?php echo esc_url(add_query_arg('paged', $i, $current_url)); ?>"><?php echo esc_html($i); ?></a>
344 <?php endif;
345 endfor;
346 if ($pagination['current_page'] < $pagination['total_pages']): ?>
347 <a class="bg-white text-gray-500 hover:bg-gray-50 px-3 py-2 text-sm font-medium border border-gray-300 rounded-md" href="<?php echo esc_url(add_query_arg('paged', $next_page, $current_url)); ?>">Next </a>
348 <?php endif; ?>
349 </div>
350 </div>
351 </div>
352 </div>
353 <?php endif; ?>
354
355 <?php // `overflow-hidden` matters here, it is not just for rounding the corners.
356 // The quotes table is wider than this card (9 columns vs the invoice list's 6)
357 // and scrolls inside its own `overflow-x-auto` wrapper. Without this, that
358 // overflow escaped every ancestor and gave the whole admin page a horizontal
359 // scrollbar. The invoice listing has always had it — see invoices/listing.php. ?>
360 <div class="bg-white shadow rounded-lg overflow-hidden">
361 <div class="border-b border-gray-200 bg-gradient-to-r from-indigo-50 to-white">
362 <nav class="flex">
363 <?php
364 // Use the counts passed from the controller
365 $status_counts = [
366 'all' => $all_count ?? 0,
367 'available' => $available_count ?? 0,
368 'draft' => $draft_count ?? 0,
369 'sent' => $sent_count ?? 0,
370 'accepted' => $accepted_count ?? 0,
371 'declined' => $declined_count ?? 0,
372 'cancelled' => $trash_count ?? 0,
373 'expired' => $expired_count ?? 0
374 ];
375
376 // Define tabs with their labels and status values
377 $tabs = [
378 'all' => __('All Quotes', 'easy-invoice'),
379 'available' => __('Available', 'easy-invoice'),
380 'draft' => __('Draft', 'easy-invoice'),
381 'sent' => __('Sent', 'easy-invoice'),
382 'accepted' => __('Accepted', 'easy-invoice'),
383 'declined' => __('Declined', 'easy-invoice'),
384 'cancelled' => __('Trash', 'easy-invoice'),
385 'expired' => __('Expired', 'easy-invoice')
386 ];
387
388 foreach ($tabs as $tab_key => $tab_label):
389 $is_active = ($view === $tab_key);
390 $count = $status_counts[$tab_key] ?? 0;
391 $class = $is_active
392 ? 'border-b-2 border-indigo-500 text-indigo-600 bg-white'
393 : 'border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300';
394 ?>
395 <a href="<?php echo esc_url(add_query_arg('view', $tab_key, remove_query_arg('status'))); ?>"
396 class="flex items-center whitespace-nowrap py-4 px-6 font-medium text-sm transition-colors duration-200 <?php echo esc_attr($class); ?>">
397 <?php if ($tab_key === 'all'): ?>
398 <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
399 <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>
400 </svg>
401 <?php elseif ($tab_key === 'draft'): ?>
402 <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
403 <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>
404 </svg>
405 <?php elseif ($tab_key === 'sent'): ?>
406 <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
407 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
408 </svg>
409 <?php elseif ($tab_key === 'accepted'): ?>
410 <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
411 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
412 </svg>
413 <?php elseif ($tab_key === 'declined'): ?>
414 <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
415 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
416 </svg>
417 <?php elseif ($tab_key === 'cancelled'): ?>
418 <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
419 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
420 </svg>
421 <?php elseif ($tab_key === 'expired'): ?>
422 <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
423 <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>
424 </svg>
425 <?php elseif ($tab_key === 'available'): ?>
426 <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
427 <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>
428 </svg>
429 <?php endif; ?>
430 <?php echo esc_html($tab_label); ?> (<?php echo esc_html($count); ?>)
431 </a>
432 <?php endforeach; ?>
433 </nav>
434 </div>
435
436 <div class="p-4 flex flex-wrap items-center justify-between bg-white border-b border-gray-100">
437 <div class="flex items-center space-x-2 my-2">
438 <?php if (easy_invoice_user_can('ei_create_quote')) : ?>
439 <form id="bulk-action-form" method="post" class="flex items-center gap-2">
440 <input type="hidden" name="nonce" value="<?php echo esc_attr(wp_create_nonce('easy_invoice_admin_nonce')); ?>">
441
442 <select name="bulk_action" aria-label="Bulk action for selected quotes" 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">
443 <option value=""><?php esc_html_e('Bulk Actions', 'easy-invoice'); ?></option>
444 <?php if ($view === 'cancelled'): ?>
445 <option value="restore"><?php esc_html_e('Restore Selected', 'easy-invoice'); ?></option>
446 <option value="delete"><?php esc_html_e('Delete Permanently', 'easy-invoice'); ?></option>
447 <?php else: ?>
448 <option value="delete"><?php esc_html_e('Delete Selected', 'easy-invoice'); ?></option>
449 <option value="trash"><?php esc_html_e('Move to Trash', 'easy-invoice'); ?></option>
450 <option value="draft"><?php esc_html_e('Move to Draft', 'easy-invoice'); ?></option>
451 <?php endif; ?>
452 <option value="export" data-requires-pro="1"><?php esc_html_e('Export Selected (Pro)', 'easy-invoice'); ?></option>
453 </select>
454
455 <button type="submit" class="inline-flex items-center justify-center h-9 px-4 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 whitespace-nowrap">
456 Apply
457 </button>
458
459 <?php if ($view === 'cancelled' && !empty($quotes)): ?>
460 <button type="button" id="empty-trash-btn" class="inline-flex items-center justify-center h-9 px-6 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 whitespace-nowrap">
461 <svg class="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
462 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
463 </svg>
464 Empty Trash
465 </button>
466 <?php endif; ?>
467 </form>
468 <?php endif; ?>
469 </div>
470
471 <div class="flex items-center gap-2">
472 <form method="get" class="flex items-center gap-2">
473 <input type="hidden" name="page" value="easy-quote-all">
474 <input type="hidden" name="view" value="<?php echo esc_attr($view); ?>">
475 <?php if (!empty($status_filter)): ?>
476 <input type="hidden" name="status" value="<?php echo esc_attr($status_filter); ?>">
477 <?php endif; ?>
478
479 <?php if (!empty($clients_list)): ?>
480 <select name="client_id" aria-label="<?php esc_attr_e('Filter by client', 'easy-invoice'); ?>"
481 class="block px-3 py-2 border border-gray-300 rounded-md bg-white text-gray-700 sm:text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"
482 onchange="this.form.submit()">
483 <option value=""><?php esc_html_e('All clients', 'easy-invoice'); ?></option>
484 <?php foreach ($clients_list as $client_option): ?>
485 <option value="<?php echo esc_attr($client_option['id']); ?>" <?php selected((int) $client_filter, (int) $client_option['id']); ?>>
486 <?php echo esc_html($client_option['name']); ?>
487 </option>
488 <?php endforeach; ?>
489 </select>
490 <?php endif; ?>
491
492 <div class="relative">
493 <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
494 <svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
495 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
496 </svg>
497 </div>
498 <input type="text"
499 name="search"
500 value="<?php echo esc_attr($search_query); ?>"
501 placeholder="<?php echo esc_js(__('Search quotes, numbers, or clients...', 'easy-invoice')); ?>"
502 class="block w-64 pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
503 </div>
504
505 <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">
506 <?php esc_html_e('Search', 'easy-invoice'); ?>
507 </button>
508
509 <?php if (!empty($search_query)): ?>
510 <a href="<?php echo esc_url(remove_query_arg('search', $_SERVER['REQUEST_URI'])); ?>" class="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">
511 <?php esc_html_e('Clear', 'easy-invoice'); ?>
512 </a>
513 <?php endif; ?>
514 </form>
515 </div>
516 </div>
517
518 <div class="overflow-x-auto">
519 <table class="min-w-full divide-y divide-gray-200 ei-quotes-table">
520 <thead class="bg-gray-50">
521 <tr>
522 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
523 <input type="checkbox" id="select-all" aria-label="Select all quotes on this page" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
524 </th>
525 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
526 Quote
527 </th>
528 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
529 Client
530 </th>
531 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
532 Amount
533 </th>
534 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
535 Status
536 </th>
537 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
538 Issue Date
539 </th>
540 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
541 Expiry Date
542 </th>
543 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
544 Logs
545 </th>
546 <th scope="col" class="relative px-6 py-3">
547 <span class="sr-only"><?php esc_html_e('Actions', 'easy-invoice'); ?></span>
548 </th>
549 </tr>
550 </thead>
551 <tbody class="bg-white divide-y divide-gray-200">
552 <?php if (empty($quotes)): ?>
553 <tr>
554 <td colspan="9" class="px-6 py-12 text-center text-gray-500">
555 <div class="flex flex-col items-center">
556 <svg class="w-12 h-12 text-gray-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
557 <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>
558 </svg>
559 <h3 class="text-lg font-medium text-gray-900 mb-2"><?php esc_html_e('No quotes found', 'easy-invoice'); ?></h3>
560 <p class="text-gray-500 mb-4"><?php esc_html_e('Get started by creating your first quote.', 'easy-invoice'); ?></p>
561 <button type="button" id="create-first-quote-btn" 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">
562 <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
563 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
564 </svg>
565 Create Your First Quote
566 </button>
567 </div>
568 </td>
569 </tr>
570 <?php else: ?>
571 <?php
572 // Force register post types and flush rewrite rules to ensure proper permalinks
573 $easy_invoice = \EasyInvoice\EasyInvoice::getInstance();
574 $easy_invoice->registerPostTypes();
575 $easy_invoice->forceFlushRewriteRules();
576
577 // Force a complete rewrite rules flush
578 flush_rewrite_rules(true);
579
580 // One-time fix: Update all existing quotes to have proper slugs
581 if (!get_option('easy_invoice_quotes_slugs_fixed_v3', false)) {
582 global $wpdb;
583 $quote_posts = $wpdb->get_results($wpdb->prepare(
584 "SELECT ID, post_title, post_name FROM {$wpdb->posts}
585 WHERE post_type = %s",
586 \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE
587 ));
588
589 foreach ($quote_posts as $post) {
590 // Generate a proper slug if empty or doesn't match our format
591 $post_name = $post->post_name ?? '';
592 if (empty($post_name) || strpos($post_name, 'easy-invoice-quote') === false) {
593 $slug = sanitize_title($post->post_title ?: 'easy-invoice-quote-' . $post->ID);
594 wp_update_post([
595 'ID' => $post->ID,
596 'post_name' => $slug
597 ]);
598 }
599 }
600
601 update_option('easy_invoice_quotes_slugs_fixed_v3', true);
602 }
603
604 // One-time fix: Publish all draft quotes to ensure proper permalinks
605 if (!get_option('easy_invoice_quotes_published_v1', false)) {
606 global $wpdb;
607 $draft_quotes = $wpdb->get_results($wpdb->prepare(
608 "SELECT ID FROM {$wpdb->posts}
609 WHERE post_type = %s AND post_status = 'draft'",
610 \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE
611 ));
612
613 foreach ($draft_quotes as $quote) {
614 wp_update_post([
615 'ID' => $quote->ID,
616 'post_status' => 'publish'
617 ]);
618 }
619
620 update_option('easy_invoice_quotes_published_v1', true);
621 }
622
623 // Ensure all quotes have proper slugs
624 foreach ($quotes as $quote):
625 $quote->ensureProperSlug();
626 ?>
627 <tr class="hover:bg-gray-50 transition-colors duration-150">
628 <td class="px-6 py-4 whitespace-nowrap">
629 <input type="checkbox" aria-label="<?php /* translators: %s: document number. */ echo esc_attr(sprintf(__('Select quote %s', 'easy-invoice'), $quote->getNumber())); ?>" class="quote-checkbox rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" value="<?php echo (int) $quote->getId(); ?>">
630 </td>
631 <td class="px-6 py-4 whitespace-nowrap">
632 <div class="flex items-center">
633 <div class="flex-shrink-0 h-10 w-10">
634 <div class="h-10 w-10 rounded-full bg-indigo-100 flex items-center justify-center">
635 <svg class="w-5 h-5 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
636 <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>
637 </svg>
638 </div>
639 </div>
640 <div class="ml-4">
641 <div class="text-sm font-medium text-gray-900">
642 <a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-quote-builder&id=' . $quote->getId())); ?>" class="text-indigo-600 hover:text-indigo-900 transition-colors duration-150">
643 <?php echo esc_html($quote->getTitle() ?: 'Untitled Quote'); ?>
644 </a>
645 </div>
646 <div class="text-sm text-gray-500">
647 <?php echo esc_html($quote->getNumber() ?: 'QT-' . $quote->getId()); ?>
648 <?php if (!in_array((string) $quote->getStatus(), ['draft'], true)) : ?>
649 <span class="ei-viewed ml-2 inline-flex items-center gap-1 <?php echo \EasyInvoice\Services\DocumentViews::summary((int) $quote->getId())['count'] ? 'text-emerald-600' : 'text-gray-400'; ?>">
650 <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><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"/></svg>
651 <?php echo esc_html(\EasyInvoice\Services\DocumentViews::label((int) $quote->getId())); ?>
652 </span>
653 <?php endif; ?>
654 </div>
655 <div class="row-actions mt-1">
656 <span class="view"><a href="<?php echo esc_url(apply_filters('easy_invoice_quote_view_url', get_permalink($quote->getId()), $quote->getId())); ?>" target="_blank"><svg class="w-3.5 h-3.5 mr-1 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><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></svg>View</a></span>
657 <span class="edit"><a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-quote-builder&id=' . $quote->getId())); ?>"><svg class="w-3.5 h-3.5 mr-1 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><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></svg>Edit</a></span>
658 <?php if ($quote->getStatus() === 'cancelled'): ?>
659 <span class="restore"><a href="#" class="restore-quote text-green-600" data-quote-id="<?php echo (int) $quote->getId(); ?>" data-quote-number="<?php echo esc_attr($quote->getNumber() ?: 'QT-' . $quote->getId()); ?>"><svg class="w-3.5 h-3.5 mr-1 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582M20 20v-5h-.581M5 9a7 7 0 0114 0c0 3.866-3.134 7-7 7a7 7 0 01-7-7zm7 7v4m0 0h-2m2 0h2"/></svg>Restore</a></span>
660 <span class="delete"><a href="#" class="delete-quote-btn text-red-600" data-quote-id="<?php echo (int) $quote->getId(); ?>" data-quote-number="<?php echo esc_attr($quote->getNumber() ?: 'QT-' . $quote->getId()); ?>"><svg class="w-3.5 h-3.5 mr-1 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>Delete Permanently</a></span>
661 <?php else: ?>
662 <span class="trash"><a href="#" class="trash-quote text-red-600" data-quote-id="<?php echo (int) $quote->getId(); ?>" data-quote-number="<?php echo esc_attr($quote->getNumber() ?: 'QT-' . $quote->getId()); ?>"><svg class="w-3.5 h-3.5 mr-1 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>Trash</a></span>
663 <span class="email"><a href="#" class="email-quote" data-quote-id="<?php echo (int) $quote->getId(); ?>" data-quote-number="<?php echo esc_attr($quote->getNumber() ?: 'QT-' . $quote->getId()); ?>"><svg class="w-3.5 h-3.5 mr-1 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>Send</a></span>
664 <span class="pdf"><a href="<?php echo esc_url(get_permalink($quote->getId())); ?>?auto_download_pdf=1" target="_blank" class="download-pdf-quote" data-quote-id="<?php echo (int) $quote->getId(); ?>" data-quote-number="<?php echo esc_attr($quote->getNumber() ?: 'QT-' . $quote->getId()); ?>"><svg class="w-3.5 h-3.5 mr-1 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><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></svg>Download</a></span>
665 <?php if (!easy_invoice_has_pro()): ?>
666 <span class="export premium-feature"><a href="#" class="export-quote-pro-placeholder text-yellow-600 font-semibold" data-quote-id="<?php echo (int) $quote->getId(); ?>" data-quote-number="<?php echo esc_attr($quote->getNumber() ?: 'QT-' . $quote->getId()); ?>"><svg class="w-3.5 h-3.5 mr-1 inline-block text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><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></svg>Export</a></span>
667 <?php endif; ?>
668 <?php if (!easy_invoice_has_pro()): ?>
669 <span class="duplicate premium-feature"><a href="#" class="duplicate-quote-pro-placeholder text-yellow-600 font-semibold" data-quote-id="<?php echo (int) $quote->getId(); ?>" data-quote-number="<?php echo esc_attr($quote->getNumber() ?: 'QT-' . $quote->getId()); ?>"><svg class="w-3.5 h-3.5 mr-1 inline-block text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 7l2 4 4 .5-3 2.5.5 4-3.5-2-3.5 2 .5-4-3-2.5 4-.5 2-4z"/></svg>Duplicate</a></span>
670 <?php endif; ?>
671
672 <?php
673 // Apply filter for Pro plugin to add secure link actions
674 $secure_actions = apply_filters('easy_invoice_quote_row_actions', [], $quote);
675 foreach ($secure_actions as $action_key => $action_html) {
676 echo '<span class="' . esc_attr($action_key) . '">' . wp_kses_post($action_html) . '</span>';
677 }
678 ?>
679 <?php endif; ?>
680 </div>
681 </div>
682 </div>
683 </td>
684 <td class="px-6 py-4 whitespace-nowrap">
685 <div class="text-sm text-gray-900">
686 <?php
687 // Get client name directly from client repository
688 $client_name = 'No Client';
689 if ($quote->getClientId() > 0) {
690 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository();
691 $client = $client_repository->find($quote->getClientId());
692 if ($client) {
693 $client_name = $client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName());
694 if (empty(trim($client_name))) {
695 $client_name = $client->getDisplayName() ?: 'Client ' . $client->getId();
696 }
697 }
698 } elseif ($quote->getCustomerName()) {
699 $client_name = $quote->getCustomerName();
700 }
701 echo esc_html($client_name);
702 ?>
703 </div>
704 <div class="text-sm text-gray-500"><?php echo esc_html($quote->getCustomerEmail() ?: ''); ?></div>
705 </td>
706 <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">
707 <?php
708 // Use QuoteFormatter for proper currency formatting
709 $formatter = new \EasyInvoice\Helpers\QuoteFormatter($quote);
710 echo esc_html($formatter->format($quote->getTotal() ?: 0));
711 ?>
712 </td>
713 <td class="px-6 py-4 whitespace-nowrap">
714 <?php
715 $status = $quote->getStatus() ?: 'draft';
716 // An open quote past its expiry date reads as expired even
717 // before the daily expiry check has flipped its status.
718 if (in_array($status, ['available', 'sent'], true) && $quote->getExpiryDate()
719 && strtotime($quote->getExpiryDate()) && gmdate('Y-m-d', strtotime($quote->getExpiryDate())) < gmdate('Y-m-d', current_time('timestamp'))) {
720 $status = 'expired';
721 }
722 $status_classes = [
723 'draft' => 'bg-gray-100 text-gray-800 ring-1 ring-gray-300',
724 'available' => 'bg-blue-100 text-blue-800 ring-1 ring-blue-300',
725 'sent' => 'bg-blue-100 text-blue-800 ring-1 ring-blue-300',
726 'accepted' => 'bg-green-100 text-green-800 ring-1 ring-green-300',
727 'declined' => 'bg-red-100 text-red-800 ring-1 ring-red-300',
728 'expired' => 'bg-yellow-100 text-yellow-800 ring-1 ring-yellow-300'
729 ];
730 $status_class = $status_classes[$status] ?? 'bg-gray-100 text-gray-800 ring-1 ring-gray-300';
731 ?>
732 <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <?php echo esc_attr($status_class); ?>">
733 <?php echo esc_html(ucfirst($status)); ?>
734 </span>
735 </td>
736 <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
737 <?php echo esc_html($quote->getIssueDate() ? gmdate('M j, Y', strtotime($quote->getIssueDate())) : ''); ?>
738 </td>
739 <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
740 <?php echo esc_html($quote->getExpiryDate() ? gmdate('M j, Y', strtotime($quote->getExpiryDate())) : ''); ?>
741 </td>
742 <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
743 <?php
744 // Get all logs for this quote
745 try {
746 $quote_log_service = new \EasyInvoice\Services\QuoteLogService();
747 $all_logs = $quote_log_service->getLogs($quote->getId());
748 $logs_count = count($all_logs);
749
750 if ($logs_count > 0): ?>
751 <button type="button"
752 class="view-logs-btn text-xs text-indigo-600 hover:text-indigo-800 font-medium"
753 data-quote-id="<?php echo (int) $quote->getId(); ?>"
754 data-quote-title="<?php echo esc_attr($quote->getTitle() ?: 'Untitled Quote'); ?>"
755 data-logs-count="<?php echo esc_attr($logs_count); ?>">
756 View all <?php echo esc_html($logs_count); ?> logs
757 </button>
758 <?php else: ?>
759 <span class="text-gray-400 text-xs"><?php esc_html_e('No activity', 'easy-invoice'); ?></span>
760 <?php endif; ?>
761 <?php } catch (\Exception $e) { ?>
762 <span class="text-gray-400 text-xs"><?php esc_html_e('Error loading logs', 'easy-invoice'); ?></span>
763 <?php } ?>
764 </td>
765 <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
766 </td>
767 </tr>
768 <?php endforeach; ?>
769 <?php endif; ?>
770 </tbody>
771 </table>
772 </div>
773 </div>
774
775 <?php if (isset($pagination) && $pagination['total_pages'] > 1): ?>
776 <div class="mt-4">
777 <div class="px-4 py-3 flex items-center justify-between">
778 <div class="flex items-center space-x-2">
779 <span class="text-sm text-gray-700">
780 Showing
781 <span class="font-medium"><?php echo (int) ((($pagination['current_page'] - 1) * $pagination['per_page']) + 1); ?></span>
782 to
783 <span class="font-medium"><?php echo (int) min($pagination['current_page'] * $pagination['per_page'], $pagination['total_quotes']); ?></span>
784 of
785 <span class="font-medium"><?php echo esc_html($pagination['total_quotes']); ?></span>
786 results
787 </span>
788 </div>
789 <div class="flex items-center space-x-2">
790 <span class="text-sm text-gray-500">Page <?php echo esc_html($pagination['current_page']); ?> of <?php echo esc_html($pagination['total_pages']); ?></span>
791 <div class="flex space-x-1">
792 <?php
793 $prev_page = $pagination['current_page'] > 1 ? $pagination['current_page'] - 1 : 1;
794 $next_page = $pagination['current_page'] < $pagination['total_pages'] ? $pagination['current_page'] + 1 : $pagination['total_pages'];
795 $current_url = add_query_arg($_GET, admin_url('admin.php'));
796 $current_url = remove_query_arg('paged', $current_url);
797 $start_page = max(1, $pagination['current_page'] - 2);
798 $end_page = min($pagination['total_pages'], $pagination['current_page'] + 2);
799 if ($pagination['current_page'] > 1): ?>
800 <a class="bg-white text-gray-500 hover:bg-gray-50 px-3 py-2 text-sm font-medium border border-gray-300 rounded-md" href="<?php echo esc_url(add_query_arg('paged', $prev_page, $current_url)); ?>"> Previous</a>
801 <?php endif;
802 for ($i = $start_page; $i <= $end_page; $i++):
803 if ($i == $pagination['current_page']): ?>
804 <span class="px-3 py-2 text-sm font-medium border border-gray-300 rounded-md bg-indigo-50 border-indigo-500 text-indigo-600"><?php echo esc_html($i); ?></span>
805 <?php else: ?>
806 <a class="px-3 py-2 text-sm font-medium border border-gray-300 bg-white text-gray-500 hover:bg-gray-50 rounded-md" href="<?php echo esc_url(add_query_arg('paged', $i, $current_url)); ?>"><?php echo esc_html($i); ?></a>
807 <?php endif;
808 endfor;
809 if ($pagination['current_page'] < $pagination['total_pages']): ?>
810 <a class="bg-white text-gray-500 hover:bg-gray-50 px-3 py-2 text-sm font-medium border border-gray-300 rounded-md" href="<?php echo esc_url(add_query_arg('paged', $next_page, $current_url)); ?>">Next </a>
811 <?php endif; ?>
812 </div>
813 </div>
814 </div>
815 </div>
816 <?php endif; ?>
817 </div>
818
819 <script>
820 jQuery(document).ready(function($) {
821 // Open modal for both buttons
822 $('#create-quote-btn, #create-first-quote-btn').on('click', function(e) {
823 e.preventDefault(); // Prevent the default link behavior
824 $('#new-quote-modal').removeClass('hidden');
825 $('#new_quote_title').focus();
826 });
827
828 // Close modal
829 $('#close-new-quote-modal, #cancel-new-quote').on('click', function() {
830 $('#new-quote-modal').addClass('hidden');
831 $('#new-quote-form')[0].reset();
832 $('#new-quote-message').addClass('hidden').html('');
833 });
834
835 // Close modal on backdrop click
836 $('#new-quote-modal').on('click', function(e) {
837 if (e.target === this) {
838 $(this).addClass('hidden');
839 $('#new-quote-form')[0].reset();
840 $('#new-quote-message').addClass('hidden').html('');
841 }
842 });
843
844 // Submit form
845 $('#new-quote-form').on('submit', function(e) {
846 e.preventDefault();
847
848 var $form = $(this);
849 var $submitBtn = $form.find('button[type="submit"]');
850 var $message = $('#new-quote-message');
851 var title = $('#new_quote_title').val().trim();
852
853 if (!title) {
854 $message.removeClass('hidden').html('<div class="text-red-600 text-sm"><?php esc_html_e('Please enter a quote title.', 'easy-invoice'); ?></div>');
855 return;
856 }
857
858 // Show loading state
859 $submitBtn.prop('disabled', true).html('Creating...');
860 $message.addClass('hidden');
861
862 $.ajax({
863 url: easyInvoice.ajaxUrl,
864 type: 'POST',
865 dataType: 'json',
866 data: {
867 action: 'easy_invoice_create_new_quote',
868 title: title,
869 nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_admin_nonce')); ?>'
870 },
871 success: function(response) {
872 if (response.success && response.data && response.data.quote_id) {
873 // Show success message below the form
874 $message.removeClass('hidden').html('<div class="text-green-600 text-sm"><?php esc_html_e('Quote created successfully! Redirecting...', 'easy-invoice'); ?></div>');
875
876 // Redirect to the builder page with the new quote ID
877 setTimeout(function() {
878 window.location.href = 'admin.php?page=easy-invoice-quote-builder&id=' + response.data.quote_id;
879 }, 1000);
880 } else {
881 $message.removeClass('hidden').html('<div class="text-red-600 text-sm">' + (response.data && response.data.message ? response.data.message : 'Error creating quote.') + '</div>');
882 $submitBtn.prop('disabled', false).html('Create Quote');
883 }
884 },
885 error: function() {
886 $message.removeClass('hidden').html('<div class="text-red-600 text-sm"><?php esc_html_e('Error connecting to server. Please try again.', 'easy-invoice'); ?></div>');
887 $submitBtn.prop('disabled', false).html('Create Quote');
888 }
889 });
890 });
891
892 // Select all checkboxes
893 $('#select-all').on('change', function() {
894 $('.quote-checkbox').prop('checked', $(this).is(':checked'));
895 updateBulkActionButton();
896 });
897
898 // Individual checkbox change
899 $(document).on('change', '.quote-checkbox', function() {
900 var totalCheckboxes = $('.quote-checkbox').length;
901 var checkedCheckboxes = $('.quote-checkbox:checked').length;
902
903 if (checkedCheckboxes === 0) {
904 $('#select-all').prop('indeterminate', false).prop('checked', false);
905 } else if (checkedCheckboxes === totalCheckboxes) {
906 $('#select-all').prop('indeterminate', false).prop('checked', true);
907 } else {
908 $('#select-all').prop('indeterminate', true);
909 }
910
911 updateBulkActionButton();
912 });
913
914 // Update bulk action button state
915 function updateBulkActionButton() {
916 var checkedCount = $('.quote-checkbox:checked').length;
917 var $applyBtn = $('#bulk-action-form button[type=submit]');
918
919 if (checkedCount > 0) {
920 $applyBtn.prop('disabled', false).text('Apply (' + checkedCount + ')');
921 } else {
922 $applyBtn.prop('disabled', true).text('Apply');
923 }
924 }
925
926 // Bulk action form submission
927 $('#bulk-action-form').on('submit', function(e) {
928 e.preventDefault();
929
930 var selectedQuotes = $('.quote-checkbox:checked').map(function() {
931 return $(this).val();
932 }).get();
933
934 var action = $(this).find('select[name="bulk_action"]').val();
935
936 if (selectedQuotes.length === 0) {
937 EasyInvoiceConfirmation.show({
938 title: 'No Quotes Selected',
939 message: 'Please select quotes to perform this action.',
940 confirmText: 'OK',
941 confirmClass: 'bg-blue-600 hover:bg-blue-700'
942 });
943 return;
944 }
945
946 if (!action) {
947 EasyInvoiceConfirmation.show({
948 title: 'No Action Selected',
949 message: 'Please select a bulk action.',
950 confirmText: 'OK',
951 confirmClass: 'bg-blue-600 hover:bg-blue-700'
952 });
953 return;
954 }
955
956 EasyInvoiceConfirmation.confirmAction(action, selectedQuotes.length + ' quote(s)', function() {
957 var $form = $(this);
958 var $btn = $form.find('button[type=submit]');
959 var originalText = $btn.text();
960
961 $btn.prop('disabled', true).text('Processing...');
962
963 $.ajax({
964 url: easyInvoice.ajaxUrl,
965 type: 'POST',
966 dataType: 'json',
967 data: {
968 action: 'easy_invoice_bulk_quote_action',
969 quote_ids: selectedQuotes,
970 bulk_action: action,
971 nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_admin_nonce')); ?>'
972 },
973 success: function(response) {
974 if (response.success) {
975 location.reload();
976 } else {
977 EasyInvoiceConfirmation.show({
978 title: 'Error',
979 message: response.data && response.data.message ? response.data.message : 'Failed to perform bulk action.',
980 confirmText: 'OK',
981 confirmClass: 'bg-red-600 hover:bg-red-700'
982 });
983 }
984 },
985 error: function() {
986 EasyInvoiceConfirmation.show({
987 title: 'Error',
988 message: 'Failed to perform bulk action.',
989 confirmText: 'OK',
990 confirmClass: 'bg-red-600 hover:bg-red-700'
991 });
992 },
993 complete: function() {
994 $btn.prop('disabled', false).text(originalText);
995 }
996 });
997 });
998 });
999
1000 // Quote row actions
1001 $(document).on('click', '.draft-quote', function(e) {
1002 e.preventDefault();
1003 const quoteId = $(this).data('quote-id');
1004 const quoteNumber = $(this).data('quote-number');
1005
1006 EasyInvoiceConfirmation.confirmAction('draft', 'quote ' + quoteNumber, function() {
1007 $.ajax({
1008 url: easyInvoice.ajaxUrl,
1009 type: 'POST',
1010 dataType: 'json',
1011 data: {
1012 action: 'easy_invoice_draft_quote',
1013 quote_id: quoteId,
1014 nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_admin_nonce')); ?>'
1015 },
1016 success: function(response) {
1017 if (response.success) {
1018 location.reload();
1019 } else {
1020 EasyInvoiceConfirmation.show({
1021 title: 'Error',
1022 message: response.data && response.data.message ? response.data.message : 'Failed to move quote to draft.',
1023 confirmText: 'OK',
1024 confirmClass: 'bg-red-600 hover:bg-red-700'
1025 });
1026 }
1027 },
1028 error: function() {
1029 EasyInvoiceConfirmation.show({
1030 title: 'Error',
1031 message: 'Failed to move quote to draft.',
1032 confirmText: 'OK',
1033 confirmClass: 'bg-red-600 hover:bg-red-700'
1034 });
1035 }
1036 });
1037 });
1038 });
1039
1040 // Convert to invoice: a confirmation, then the draft opens in the builder.
1041 $(document).on('click', '.convert-quote', function(e) {
1042 e.preventDefault();
1043 var quoteId = $(this).data('quote-id'), quoteNumber = $(this).data('quote-number');
1044 var run = function() {
1045 $.post(ajaxurl, { action: 'easy_invoice_convert_quote', quote_id: quoteId, nonce: '<?php echo esc_js(wp_create_nonce('easy_invoice_admin_nonce')); ?>' }, function(response) {
1046 if (response && response.success) {
1047 if (typeof EasyInvoiceToast !== 'undefined') { EasyInvoiceToast.success(response.data.message); }
1048 window.location.href = response.data.redirect || ('<?php echo esc_url(admin_url('admin.php?page=easy-invoice-builder&invoice_id=')); ?>' + response.data.invoice_id);
1049 } else if (typeof EasyInvoiceToast !== 'undefined') {
1050 EasyInvoiceToast.error((response && response.data && response.data.message) || '<?php echo esc_js(__('The invoice could not be created.', 'easy-invoice')); ?>');
1051 }
1052 });
1053 };
1054 if (typeof EasyInvoiceConfirmation !== 'undefined') {
1055 EasyInvoiceConfirmation.show({ title: '<?php echo esc_js(__('Convert to invoice', 'easy-invoice')); ?>', message: '<?php echo esc_js(__('Create a draft invoice from quote %s? The quote is marked accepted.', 'easy-invoice')); ?>'.replace('%s', quoteNumber), confirmText: '<?php echo esc_js(__('Create invoice', 'easy-invoice')); ?>', onConfirm: run });
1056 } else if (window.confirm('<?php echo esc_js(__('Create a draft invoice from this quote?', 'easy-invoice')); ?>')) { run(); }
1057 });
1058
1059 $(document).on('click', '.trash-quote', function(e) {
1060 e.preventDefault();
1061 const quoteId = $(this).data('quote-id');
1062 const quoteNumber = $(this).data('quote-number');
1063
1064 EasyInvoiceConfirmation.confirmAction('trash', 'quote ' + quoteNumber, function() {
1065 $.ajax({
1066 url: easyInvoice.ajaxUrl,
1067 type: 'POST',
1068 dataType: 'json',
1069 data: {
1070 action: 'easy_invoice_trash_quote',
1071 quote_id: quoteId,
1072 nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_admin_nonce')); ?>'
1073 },
1074 success: function(response) {
1075 if (response.success) {
1076 location.reload();
1077 } else {
1078 EasyInvoiceConfirmation.show({
1079 title: 'Error',
1080 message: response.data && response.data.message ? response.data.message : 'Failed to move quote to trash.',
1081 confirmText: 'OK',
1082 confirmClass: 'bg-red-600 hover:bg-red-700'
1083 });
1084 }
1085 },
1086 error: function() {
1087 EasyInvoiceConfirmation.show({
1088 title: 'Error',
1089 message: 'Failed to move quote to trash.',
1090 confirmText: 'OK',
1091 confirmClass: 'bg-red-600 hover:bg-red-700'
1092 });
1093 }
1094 });
1095 });
1096 });
1097
1098 $(document).on('click', '.email-quote', function(e) {
1099 e.preventDefault();
1100 const quoteId = $(this).data('quote-id');
1101 const quoteNumber = $(this).data('quote-number');
1102
1103 // Use the new confirmation system instead of browser confirm
1104 if (typeof EasyInvoiceConfirmation !== 'undefined') {
1105 EasyInvoiceConfirmation.confirmAction('send email', 'quote ' + quoteNumber, function() {
1106 // Show loading state
1107 var $link = $(this);
1108 var originalText = $link.text();
1109 $link.text("Sending...").css("opacity", "0.7");
1110
1111 // Send AJAX request
1112 $.ajax({
1113 url: easyInvoice.ajaxUrl,
1114 type: "POST",
1115 data: {
1116 action: "easy_invoice_send_quote_email",
1117 quote_id: quoteId,
1118 nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_send_quote_email')); ?>'
1119 },
1120 success: function(response) {
1121 if (response.success) {
1122 EasyInvoiceToast.success("Email sent successfully!");
1123 } else {
1124 EasyInvoiceToast.error(response.data.message || "Error sending email");
1125 }
1126 // Reset link text
1127 $link.text(originalText).css("opacity", "1");
1128 },
1129 error: function() {
1130 EasyInvoiceToast.error("Error connecting to server");
1131 // Reset link text
1132 $link.text(originalText).css("opacity", "1");
1133 }
1134 });
1135 });
1136 } else {
1137 // Fallback to browser confirm if toast system not available
1138 if (confirm("Are you sure you want to send quote " + quoteNumber + " via email?")) {
1139 // Show loading state
1140 var $link = $(this);
1141 var originalText = $link.text();
1142 $link.text("Sending...").css("opacity", "0.7");
1143
1144 // Send AJAX request
1145 $.ajax({
1146 url: easyInvoice.ajaxUrl,
1147 type: "POST",
1148 data: {
1149 action: "easy_invoice_send_quote_email",
1150 quote_id: quoteId,
1151 nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_send_quote_email')); ?>'
1152 },
1153 success: function(response) {
1154 if (response.success) {
1155 EasyInvoiceToast.success("Email sent successfully!");
1156 } else {
1157 // Handle both response.data.message and response.data (direct string)
1158 const errorMessage = (response.data && response.data.message) ? response.data.message : (response.data || "Error sending email");
1159 EasyInvoiceToast.error(errorMessage);
1160 }
1161 // Reset link text
1162 $link.text(originalText).css("opacity", "1");
1163 },
1164 error: function() {
1165 EasyInvoiceToast.error("Error connecting to server");
1166 // Reset link text
1167 $link.text(originalText).css("opacity", "1");
1168 }
1169 });
1170 }
1171 }
1172 });
1173
1174 // Download PDF: let the anchor's native href do the job.
1175 //
1176 // Historically this handler intercepted the click and did an
1177 // admin-ajax round-trip that minted a per-user nonce and opened
1178 // the resulting admin-ajax URL in a new tab. Same failure modes
1179 // as the invoice listing — see that template for the full
1180 // rationale. Removing the interceptor collapses three server
1181 // round-trips into one and sidesteps every failure mode. The
1182 // `easy_invoice_download_quote_pdf` / `easy_invoice_generate_quote_pdf`
1183 // handlers are still registered on the server for any external
1184 // integration that used them (they now have their own robust
1185 // 3-path auth + JS redirect fallback — see
1186 // EasyInvoiceAjax::generateQuotePdf), but this button no longer
1187 // depends on them.
1188
1189 // Delete quote
1190 $(document).on('click', '.delete-quote-btn', function() {
1191 const quoteId = $(this).data('quote-id');
1192 const $row = $(this).closest('tr');
1193
1194 EasyInvoiceConfirmation.confirmAction('delete', 'this quote', function() {
1195 $.ajax({
1196 url: easyInvoice.ajaxUrl,
1197 type: 'POST',
1198 dataType: 'json',
1199 data: {
1200 action: 'easy_invoice_delete_quote',
1201 quote_id: quoteId,
1202 nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_admin_nonce')); ?>'
1203 },
1204 success: function(response) {
1205 if (response.success) {
1206 $row.fadeOut(300, function() {
1207 $(this).remove();
1208 // Check if table is empty
1209 if ($('tbody tr').length === 0) {
1210 location.reload();
1211 }
1212 });
1213 } else {
1214 EasyInvoiceConfirmation.show({
1215 title: 'Error',
1216 message: response.data && response.data.message ? response.data.message : 'Failed to delete quote.',
1217 confirmText: 'OK',
1218 confirmClass: 'bg-red-600 hover:bg-red-700'
1219 });
1220 }
1221 },
1222 error: function() {
1223 EasyInvoiceConfirmation.show({
1224 title: 'Error',
1225 message: 'Failed to delete quote.',
1226 confirmText: 'OK',
1227 confirmClass: 'bg-red-600 hover:bg-red-700'
1228 });
1229 }
1230 });
1231 });
1232 });
1233
1234 // Status filter change
1235 $('#status-filter').on('change', function() {
1236 var status = $(this).val();
1237 var currentUrl = new URL(window.location);
1238
1239 if (status) {
1240 currentUrl.searchParams.set('status', status);
1241 } else {
1242 currentUrl.searchParams.delete('status');
1243 }
1244
1245 window.location.href = currentUrl.toString();
1246 });
1247
1248 // Initialize
1249 updateBulkActionButton();
1250
1251 // Empty Trash functionality
1252 $('#empty-trash-btn').on('click', function(e) {
1253 e.preventDefault();
1254
1255 // Check if EasyInvoiceConfirmation is available
1256 if (typeof EasyInvoiceConfirmation === 'undefined') {
1257 console.error('EasyInvoiceConfirmation is not defined');
1258 EasyInvoiceToast.error('Confirmation modal not loaded. Please refresh the page and try again.');
1259 return;
1260 }
1261
1262 EasyInvoiceConfirmation.show({
1263 title: 'Empty Trash',
1264 message: 'Are you sure you want to permanently delete all quotes in the trash? This action cannot be undone.',
1265 confirmText: 'Empty Trash',
1266 confirmClass: 'bg-red-600 hover:bg-red-700',
1267 cancelText: 'Cancel',
1268 cancelClass: 'bg-gray-300 hover:bg-gray-400',
1269 onConfirm: function() {
1270 // User confirmed - proceed with emptying trash
1271 var $btn = $('#empty-trash-btn');
1272 var originalText = $btn.text();
1273
1274 $btn.prop('disabled', true).text('Emptying...');
1275
1276 $.ajax({
1277 url: easyInvoice.ajaxUrl,
1278 type: 'POST',
1279 dataType: 'json',
1280 data: {
1281 action: 'easy_invoice_empty_trash',
1282 nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_nonce')); ?>'
1283 },
1284 success: function(response) {
1285 if (response.success) {
1286 EasyInvoiceToast.success('Trash emptied successfully');
1287 setTimeout(function() {
1288 location.reload();
1289 }, 1000);
1290 } else {
1291 EasyInvoiceToast.error(response.data && response.data.message ? response.data.message : 'Failed to empty trash.');
1292 }
1293 },
1294 error: function(xhr, status, error) {
1295 console.error('Empty trash AJAX error:', error);
1296 EasyInvoiceToast.error('Failed to empty trash. Please try again.');
1297 },
1298 complete: function() {
1299 $btn.prop('disabled', false).text(originalText);
1300 }
1301 });
1302 }
1303 });
1304 });
1305
1306 $(document).on('click', '.restore-quote', function(e) {
1307 e.preventDefault();
1308 const quoteId = $(this).data('quote-id');
1309 const quoteNumber = $(this).data('quote-number');
1310 EasyInvoiceConfirmation.confirmAction('restore', 'quote ' + quoteNumber, function() {
1311 $.ajax({
1312 url: easyInvoice.ajaxUrl,
1313 type: 'POST',
1314 dataType: 'json',
1315 data: {
1316 action: 'easy_invoice_restore_quote',
1317 quote_id: quoteId,
1318 nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_admin_nonce')); ?>'
1319 },
1320 success: function(response) {
1321 if (response.success) {
1322 location.reload();
1323 } else {
1324 EasyInvoiceConfirmation.show({
1325 title: 'Error',
1326 message: response.data && response.data.message ? response.data.message : 'Failed to restore quote.',
1327 confirmText: 'OK',
1328 confirmClass: 'bg-red-600 hover:bg-red-700'
1329 });
1330 }
1331 },
1332 error: function() {
1333 EasyInvoiceConfirmation.show({
1334 title: 'Error',
1335 message: 'Failed to restore quote.',
1336 confirmText: 'OK',
1337 confirmClass: 'bg-red-600 hover:bg-red-700'
1338 });
1339 }
1340 });
1341 });
1342 });
1343
1344 // Duplicate quote functionality
1345 $(document).on('click', '.duplicate-quote-pro-placeholder', function(e) {
1346 e.preventDefault();
1347 EasyInvoiceConfirmation.showFeatureUpgrade('Quote Duplication');
1348 });
1349
1350 // Export quote functionality
1351 $(document).on('click', '.export-quote-pro-placeholder', function(e) {
1352 e.preventDefault();
1353 EasyInvoiceConfirmation.showFeatureUpgrade('Quote Export', 'Export your quotes to CSV, Excel, or PDF format for backup, analysis, or sharing with your team.');
1354 });
1355
1356 $(document).on('click', '.duplicate-quote', function(e) {
1357 e.preventDefault();
1358 const quoteId = $(this).data('quote-id');
1359 const quoteNumber = $(this).data('quote-number');
1360
1361 EasyInvoiceConfirmation.confirmAction('duplicate', 'quote ' + quoteNumber, function() {
1362 $.ajax({
1363 url: easyInvoice.ajaxUrl,
1364 type: 'POST',
1365 dataType: 'json',
1366 data: {
1367 action: 'easy_invoice_duplicate_quote',
1368 quote_id: quoteId,
1369 nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_admin_nonce')); ?>'
1370 },
1371 success: function(response) {
1372 if (response.success && response.data && response.data.quote_id) {
1373 EasyInvoiceConfirmation.show({
1374 title: 'Success',
1375 message: 'Quote duplicated successfully! You will be redirected to the new quote.',
1376 confirmText: 'OK',
1377 confirmClass: 'bg-green-600 hover:bg-green-700',
1378 onConfirm: function() {
1379 window.location.href = 'admin.php?page=easy-invoice-quote-builder&id=' + response.data.quote_id;
1380 }
1381 });
1382 } else {
1383 EasyInvoiceConfirmation.show({
1384 title: 'Error',
1385 message: response.data && response.data.message ? response.data.message : 'Failed to duplicate quote.',
1386 confirmText: 'OK',
1387 confirmClass: 'bg-red-600 hover:bg-red-700'
1388 });
1389 }
1390 },
1391 error: function() {
1392 EasyInvoiceConfirmation.show({
1393 title: 'Error',
1394 message: 'Failed to duplicate quote.',
1395 confirmText: 'OK',
1396 confirmClass: 'bg-red-600 hover:bg-red-700'
1397 });
1398 }
1399 });
1400 });
1401 });
1402
1403 // View Logs functionality
1404 $(document).on('click', '.view-logs-btn', function(e) {
1405 e.preventDefault();
1406 const quoteId = $(this).data('quote-id');
1407 const quoteTitle = $(this).data('quote-title');
1408 const logsCount = $(this).data('logs-count');
1409
1410 // Show loading state
1411 $('#logs-modal .modal-body').html('<div class="flex justify-center items-center py-8"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div></div>');
1412 $('#logs-modal').removeClass('hidden');
1413
1414 // Fetch logs via AJAX
1415 $.ajax({
1416 url: easyInvoice.ajaxUrl,
1417 type: 'POST',
1418 dataType: 'json',
1419 data: {
1420 action: 'easy_invoice_get_quote_logs',
1421 quote_id: quoteId,
1422 nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_admin_nonce')); ?>'
1423 },
1424 success: function(response) {
1425 if (response.success && response.data && response.data.logs) {
1426 let logsHtml = '<div class="space-y-4">';
1427
1428 response.data.logs.forEach(function(log) {
1429 const date = new Date(log.created_date);
1430 const formattedDate = date.toLocaleDateString('en-US', {
1431 year: 'numeric',
1432 month: 'short',
1433 day: 'numeric',
1434 hour: '2-digit',
1435 minute: '2-digit'
1436 });
1437
1438 logsHtml += `
1439 <div class="border-l-4 border-indigo-500 pl-4 py-2">
1440 <div class="flex items-start justify-between">
1441 <div class="flex-1">
1442 <div class="text-sm font-medium text-gray-900">${log.description}</div>
1443 <div class="text-xs text-gray-500 mt-1">
1444 <span class="font-medium">${log.user_name}</span> • ${formattedDate}
1445 </div>
1446 ${log.ip_address ? `<div class="text-xs text-gray-400 mt-1">IP: ${log.ip_address}</div>` : ''}
1447 </div>
1448 <div class="ml-4">
1449 <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800">
1450 ${log.action}
1451 </span>
1452 </div>
1453 </div>
1454 </div>
1455 `;
1456 });
1457
1458 logsHtml += '</div>';
1459 $('#logs-modal .modal-body').html(logsHtml);
1460 } else {
1461 $('#logs-modal .modal-body').html('<div class="text-center py-8 text-gray-500"><?php esc_html_e('No logs found for this quote.', 'easy-invoice'); ?></div>');
1462 }
1463 },
1464 error: function() {
1465 $('#logs-modal .modal-body').html('<div class="text-center py-8 text-red-500"><?php esc_html_e('Error loading logs. Please try again.', 'easy-invoice'); ?></div>');
1466 }
1467 });
1468 });
1469
1470 // Close logs modal
1471 $(document).on('click', '#close-logs-modal', function(e) {
1472 e.preventDefault();
1473 $('#logs-modal').addClass('hidden');
1474 });
1475
1476 // Close modal when clicking outside
1477 $(document).on('click', '#logs-modal', function(e) {
1478 if (e.target === this) {
1479 $('#logs-modal').addClass('hidden');
1480 }
1481 });
1482 });
1483 </script>
1484
1485 <style>
1486 /* Modern row actions styling (copied from invoice listing) */
1487 /* Team roles: controls a member may not use are not drawn (the endpoints check again). */
1488 <?php if (!easy_invoice_user_can('ei_create_quote')) : ?>.row-actions .edit, .row-actions .duplicate, .row-actions .convert { display: none; }<?php endif; ?>
1489 <?php if (!easy_invoice_user_can('ei_delete_quote')) : ?>.row-actions .trash, .row-actions .delete, .row-actions .restore { display: none; }<?php endif; ?>
1490 <?php if (!easy_invoice_user_can('ei_send_invoice')) : ?>.row-actions .email { display: none; }<?php endif; ?>
1491 /* Nine columns: the title cell may wrap, and the dates sit closer together. */
1492 .ei-quotes-table td:nth-child(2) { white-space: normal; }
1493 .ei-quotes-table td:nth-child(2) .ei-viewed { white-space: nowrap; }
1494 .ei-quotes-table th, .ei-quotes-table td { padding-left: 1rem; padding-right: 1rem; }
1495 .row-actions {
1496 visibility: hidden;
1497 font-size: 12px;
1498 padding-top: 4px;
1499 display: flex;
1500 flex-wrap: wrap; /* a long action list must not widen the column past the screen */
1501 gap: 4px 8px;
1502 }
1503 tr:hover .row-actions,
1504 tr:focus-within .row-actions {
1505 visibility: visible;
1506 }
1507
1508 /*
1509 * Row actions were revealed on `tr:hover` only, which made them unreachable for
1510 * two whole groups of users:
1511 *
1512 * - Keyboard users. `visibility: hidden` also removes elements from the tab
1513 * order, so View / Edit / Trash / Send Email / Download PDF / Duplicate could
1514 * never be focused at all. `:focus-within` above fixes that — tabbing into the
1515 * row now reveals them.
1516 * - Touch users. There is no hover on a phone or tablet, so every row action was
1517 * permanently invisible and there was no other way to reach them.
1518 *
1519 * On pointer-less devices, and on the narrow widths WordPress itself treats as
1520 * mobile admin, the actions are simply always visible.
1521 */
1522 @media (hover: none), (max-width: 782px) {
1523 .row-actions {
1524 visibility: visible;
1525 }
1526 }
1527 .row-actions span a {
1528 color: #6366f1; /* indigo-500 */
1529 text-decoration: none;
1530 transition: all 0.2s;
1531 display: inline-flex;
1532 align-items: center;
1533 }
1534 .row-actions span a:hover {
1535 color: #4f46e5; /* indigo-600 */
1536 text-decoration: none;
1537 }
1538 .row-actions span a.text-red-600 {
1539 color: #dc2626; /* red-600 */
1540 }
1541 .row-actions span a.text-red-600:hover {
1542 color: #b91c1c; /* red-700 */
1543 }
1544 .row-actions span:not(:last-child)::after {
1545 content: "";
1546 display: inline-block;
1547 width: 3px;
1548 height: 3px;
1549 border-radius: 50%;
1550 background-color: #d1d5db; /* gray-300 */
1551 margin-left: 8px;
1552 vertical-align: middle;
1553 }
1554
1555 #easy-invoice-confirmation-modal .bg-white {
1556 max-height: 90vh;
1557 overflow-y: auto;
1558 }
1559 #easy-invoice-confirmation-modal .flex.justify-end {
1560 flex-wrap: wrap;
1561 }
1562
1563 .premium-feature .premium-duplicate-btn {
1564 border: 1.5px solid #FFD700;
1565 background: linear-gradient(90deg, #fffbe6 0%, #fff8dc 100%);
1566 box-shadow: 0 0 6px 0 #ffe06633;
1567 position: relative;
1568 transition: box-shadow 0.2s, border-color 0.2s;
1569 }
1570 .premium-feature .premium-duplicate-btn:hover {
1571 box-shadow: 0 0 16px 2px #ffd70099, 0 0 0 2px #fffbe6;
1572 border-color: #FFA500;
1573 animation: shimmer 1.2s linear infinite;
1574 }
1575 @keyframes shimmer {
1576 0% { box-shadow: 0 0 6px 0 #ffe06633; }
1577 50% { box-shadow: 0 0 24px 4px #ffd700cc; }
1578 100% { box-shadow: 0 0 6px 0 #ffe06633; }
1579 }
1580 .premium-feature .premium-tooltip {
1581 opacity: 0;
1582 pointer-events: none;
1583 position: absolute;
1584 left: 50%;
1585 top: 120%;
1586 transform: translateX(-50%);
1587 background: #222;
1588 color: #fffbe6;
1589 padding: 3px 10px;
1590 border-radius: 4px;
1591 font-size: 11px;
1592 white-space: nowrap;
1593 z-index: 10;
1594 transition: opacity 0.2s;
1595 box-shadow: 0 2px 8px #0002;
1596 }
1597 .premium-feature .group:hover .premium-tooltip {
1598 opacity: 1;
1599 }
1600 .premium-feature .premium-duplicate-icon {
1601 filter: drop-shadow(0 0 2px #ffd700);
1602 }
1603
1604 /* Premium Export Button Styling - Minimal */
1605 .premium-export-btn {
1606 border-color: #d1d5db;
1607 transition: all 0.2s ease;
1608 }
1609
1610 .premium-export-btn:hover {
1611 border-color: #9ca3af;
1612 background-color: #f9fafb;
1613 }
1614
1615 .premium-export-btn .crown-icon {
1616 filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.2));
1617 color: #9333ea;
1618 transition: all 0.3s ease;
1619 }
1620
1621 .premium-export-btn:hover .crown-icon {
1622 filter: drop-shadow(0 2px 6px rgba(147, 51, 234, 0.3));
1623 transform: scale(1.1);
1624 color: #7c3aed;
1625 }
1626 </style>
1627