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

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

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