PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.1.2
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.1.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 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.1.2, at templates/quotes/listing.php

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