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

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

1,665 lines 89.9 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 $(document).on('click', '.download-pdf-quote', function(e) {
1220 e.preventDefault();
1221 const quoteId = $(this).data('quote-id');
1222 const quoteNumber = $(this).data('quote-number');
1223
1224 // Show loading state
1225 var $link = $(this);
1226 var originalText = $link.text();
1227 $link.text("Generating PDF...").css("opacity", "0.7");
1228
1229 // Get quote data via AJAX and generate PDF
1230 $.ajax({
1231 url: easyInvoice.ajaxUrl,
1232 type: "POST",
1233 data: {
1234 action: "easy_invoice_download_quote_pdf",
1235 quote_id: quoteId,
1236 nonce: easyInvoice.nonce
1237 },
1238 success: function(response) {
1239 if (response.success && response.data.download_url) {
1240 // Open the download URL in a new tab
1241 window.open(response.data.download_url, '_blank');
1242 EasyInvoiceToast.success("PDF download started");
1243 } else {
1244 EasyInvoiceToast.error(response.data.message || "Error preparing PDF data");
1245 }
1246 // Reset link text
1247 $link.text(originalText).css("opacity", "1");
1248 },
1249 error: function() {
1250 EasyInvoiceToast.error("Error connecting to server");
1251 // Reset link text
1252 $link.text(originalText).css("opacity", "1");
1253 }
1254 });
1255 });
1256
1257 // Delete quote
1258 $(document).on('click', '.delete-quote-btn', function() {
1259 const quoteId = $(this).data('quote-id');
1260 const $row = $(this).closest('tr');
1261
1262 EasyInvoiceConfirmation.confirmAction('delete', 'this quote', function() {
1263 $.ajax({
1264 url: easyInvoice.ajaxUrl,
1265 type: 'POST',
1266 dataType: 'json',
1267 data: {
1268 action: 'easy_invoice_delete_quote',
1269 quote_id: quoteId,
1270 nonce: '<?php echo wp_create_nonce('easy_invoice_admin_nonce'); ?>'
1271 },
1272 success: function(response) {
1273 if (response.success) {
1274 $row.fadeOut(300, function() {
1275 $(this).remove();
1276 // Check if table is empty
1277 if ($('tbody tr').length === 0) {
1278 location.reload();
1279 }
1280 });
1281 } else {
1282 EasyInvoiceConfirmation.show({
1283 title: 'Error',
1284 message: response.data && response.data.message ? response.data.message : 'Failed to delete quote.',
1285 confirmText: 'OK',
1286 confirmClass: 'bg-red-600 hover:bg-red-700'
1287 });
1288 }
1289 },
1290 error: function() {
1291 EasyInvoiceConfirmation.show({
1292 title: 'Error',
1293 message: 'Failed to delete quote.',
1294 confirmText: 'OK',
1295 confirmClass: 'bg-red-600 hover:bg-red-700'
1296 });
1297 }
1298 });
1299 });
1300 });
1301
1302 // Status filter change
1303 $('#status-filter').on('change', function() {
1304 var status = $(this).val();
1305 var currentUrl = new URL(window.location);
1306
1307 if (status) {
1308 currentUrl.searchParams.set('status', status);
1309 } else {
1310 currentUrl.searchParams.delete('status');
1311 }
1312
1313 window.location.href = currentUrl.toString();
1314 });
1315
1316 // Initialize
1317 updateBulkActionButton();
1318
1319 // Empty Trash functionality
1320 $('#empty-trash-btn').on('click', function(e) {
1321 e.preventDefault();
1322
1323 // Check if EasyInvoiceConfirmation is available
1324 if (typeof EasyInvoiceConfirmation === 'undefined') {
1325 console.error('EasyInvoiceConfirmation is not defined');
1326 EasyInvoiceToast.error('Confirmation modal not loaded. Please refresh the page and try again.');
1327 return;
1328 }
1329
1330 EasyInvoiceConfirmation.show({
1331 title: 'Empty Trash',
1332 message: 'Are you sure you want to permanently delete all quotes in the trash? This action cannot be undone.',
1333 confirmText: 'Empty Trash',
1334 confirmClass: 'bg-red-600 hover:bg-red-700',
1335 cancelText: 'Cancel',
1336 cancelClass: 'bg-gray-300 hover:bg-gray-400',
1337 onConfirm: function() {
1338 // User confirmed - proceed with emptying trash
1339 var $btn = $('#empty-trash-btn');
1340 var originalText = $btn.text();
1341
1342 $btn.prop('disabled', true).text('Emptying...');
1343
1344 $.ajax({
1345 url: easyInvoice.ajaxUrl,
1346 type: 'POST',
1347 dataType: 'json',
1348 data: {
1349 action: 'easy_invoice_empty_trash',
1350 nonce: '<?php echo wp_create_nonce('easy_invoice_nonce'); ?>'
1351 },
1352 success: function(response) {
1353 if (response.success) {
1354 EasyInvoiceToast.success('Trash emptied successfully');
1355 setTimeout(function() {
1356 location.reload();
1357 }, 1000);
1358 } else {
1359 EasyInvoiceToast.error(response.data && response.data.message ? response.data.message : 'Failed to empty trash.');
1360 }
1361 },
1362 error: function(xhr, status, error) {
1363 console.error('Empty trash AJAX error:', error);
1364 EasyInvoiceToast.error('Failed to empty trash. Please try again.');
1365 },
1366 complete: function() {
1367 $btn.prop('disabled', false).text(originalText);
1368 }
1369 });
1370 }
1371 });
1372 });
1373
1374 $(document).on('click', '.restore-quote', function(e) {
1375 e.preventDefault();
1376 const quoteId = $(this).data('quote-id');
1377 const quoteNumber = $(this).data('quote-number');
1378 EasyInvoiceConfirmation.confirmAction('restore', 'quote ' + quoteNumber, function() {
1379 $.ajax({
1380 url: easyInvoice.ajaxUrl,
1381 type: 'POST',
1382 dataType: 'json',
1383 data: {
1384 action: 'easy_invoice_restore_quote',
1385 quote_id: quoteId,
1386 nonce: '<?php echo wp_create_nonce('easy_invoice_admin_nonce'); ?>'
1387 },
1388 success: function(response) {
1389 if (response.success) {
1390 location.reload();
1391 } else {
1392 EasyInvoiceConfirmation.show({
1393 title: 'Error',
1394 message: response.data && response.data.message ? response.data.message : 'Failed to restore quote.',
1395 confirmText: 'OK',
1396 confirmClass: 'bg-red-600 hover:bg-red-700'
1397 });
1398 }
1399 },
1400 error: function() {
1401 EasyInvoiceConfirmation.show({
1402 title: 'Error',
1403 message: 'Failed to restore quote.',
1404 confirmText: 'OK',
1405 confirmClass: 'bg-red-600 hover:bg-red-700'
1406 });
1407 }
1408 });
1409 });
1410 });
1411
1412 // Duplicate quote functionality
1413 $(document).on('click', '.duplicate-quote-pro-placeholder', function(e) {
1414 e.preventDefault();
1415 EasyInvoiceConfirmation.showFeatureUpgrade('Quote Duplication');
1416 });
1417
1418 // Export quote functionality
1419 $(document).on('click', '.export-quote-pro-placeholder', function(e) {
1420 e.preventDefault();
1421 EasyInvoiceConfirmation.showFeatureUpgrade('Quote Export', 'Export your quotes to CSV, Excel, or PDF format for backup, analysis, or sharing with your team.');
1422 });
1423
1424 $(document).on('click', '.duplicate-quote', function(e) {
1425 e.preventDefault();
1426 const quoteId = $(this).data('quote-id');
1427 const quoteNumber = $(this).data('quote-number');
1428
1429 EasyInvoiceConfirmation.confirmAction('duplicate', 'quote ' + quoteNumber, function() {
1430 $.ajax({
1431 url: easyInvoice.ajaxUrl,
1432 type: 'POST',
1433 dataType: 'json',
1434 data: {
1435 action: 'easy_invoice_duplicate_quote',
1436 quote_id: quoteId,
1437 nonce: '<?php echo wp_create_nonce('easy_invoice_admin_nonce'); ?>'
1438 },
1439 success: function(response) {
1440 if (response.success && response.data && response.data.quote_id) {
1441 EasyInvoiceConfirmation.show({
1442 title: 'Success',
1443 message: 'Quote duplicated successfully! You will be redirected to the new quote.',
1444 confirmText: 'OK',
1445 confirmClass: 'bg-green-600 hover:bg-green-700',
1446 onConfirm: function() {
1447 window.location.href = 'admin.php?page=easy-invoice-quote-builder&id=' + response.data.quote_id;
1448 }
1449 });
1450 } else {
1451 EasyInvoiceConfirmation.show({
1452 title: 'Error',
1453 message: response.data && response.data.message ? response.data.message : 'Failed to duplicate quote.',
1454 confirmText: 'OK',
1455 confirmClass: 'bg-red-600 hover:bg-red-700'
1456 });
1457 }
1458 },
1459 error: function() {
1460 EasyInvoiceConfirmation.show({
1461 title: 'Error',
1462 message: 'Failed to duplicate quote.',
1463 confirmText: 'OK',
1464 confirmClass: 'bg-red-600 hover:bg-red-700'
1465 });
1466 }
1467 });
1468 });
1469 });
1470
1471 // View Logs functionality
1472 $(document).on('click', '.view-logs-btn', function(e) {
1473 e.preventDefault();
1474 const quoteId = $(this).data('quote-id');
1475 const quoteTitle = $(this).data('quote-title');
1476 const logsCount = $(this).data('logs-count');
1477
1478 // Show loading state
1479 $('#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>');
1480 $('#logs-modal').removeClass('hidden');
1481
1482 // Fetch logs via AJAX
1483 $.ajax({
1484 url: easyInvoice.ajaxUrl,
1485 type: 'POST',
1486 dataType: 'json',
1487 data: {
1488 action: 'easy_invoice_get_quote_logs',
1489 quote_id: quoteId,
1490 nonce: '<?php echo wp_create_nonce('easy_invoice_admin_nonce'); ?>'
1491 },
1492 success: function(response) {
1493 if (response.success && response.data && response.data.logs) {
1494 let logsHtml = '<div class="space-y-4">';
1495
1496 response.data.logs.forEach(function(log) {
1497 const date = new Date(log.created_date);
1498 const formattedDate = date.toLocaleDateString('en-US', {
1499 year: 'numeric',
1500 month: 'short',
1501 day: 'numeric',
1502 hour: '2-digit',
1503 minute: '2-digit'
1504 });
1505
1506 logsHtml += `
1507 <div class="border-l-4 border-indigo-500 pl-4 py-2">
1508 <div class="flex items-start justify-between">
1509 <div class="flex-1">
1510 <div class="text-sm font-medium text-gray-900">${log.description}</div>
1511 <div class="text-xs text-gray-500 mt-1">
1512 <span class="font-medium">${log.user_name}</span> • ${formattedDate}
1513 </div>
1514 ${log.ip_address ? `<div class="text-xs text-gray-400 mt-1">IP: ${log.ip_address}</div>` : ''}
1515 </div>
1516 <div class="ml-4">
1517 <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800">
1518 ${log.action}
1519 </span>
1520 </div>
1521 </div>
1522 </div>
1523 `;
1524 });
1525
1526 logsHtml += '</div>';
1527 $('#logs-modal .modal-body').html(logsHtml);
1528 } else {
1529 $('#logs-modal .modal-body').html('<div class="text-center py-8 text-gray-500">No logs found for this quote.</div>');
1530 }
1531 },
1532 error: function() {
1533 $('#logs-modal .modal-body').html('<div class="text-center py-8 text-red-500">Error loading logs. Please try again.</div>');
1534 }
1535 });
1536 });
1537
1538 // Close logs modal
1539 $(document).on('click', '#close-logs-modal', function(e) {
1540 e.preventDefault();
1541 $('#logs-modal').addClass('hidden');
1542 });
1543
1544 // Close modal when clicking outside
1545 $(document).on('click', '#logs-modal', function(e) {
1546 if (e.target === this) {
1547 $('#logs-modal').addClass('hidden');
1548 }
1549 });
1550 });
1551 </script>
1552
1553 <style>
1554 /* Modern row actions styling (copied from invoice listing) */
1555 .row-actions {
1556 visibility: hidden;
1557 font-size: 12px;
1558 padding-top: 4px;
1559 display: flex;
1560 gap: 8px;
1561 }
1562 tr:hover .row-actions {
1563 visibility: visible;
1564 }
1565 .row-actions span a {
1566 color: #6366f1; /* indigo-500 */
1567 text-decoration: none;
1568 transition: all 0.2s;
1569 display: inline-flex;
1570 align-items: center;
1571 }
1572 .row-actions span a:hover {
1573 color: #4f46e5; /* indigo-600 */
1574 text-decoration: none;
1575 }
1576 .row-actions span a.text-red-600 {
1577 color: #dc2626; /* red-600 */
1578 }
1579 .row-actions span a.text-red-600:hover {
1580 color: #b91c1c; /* red-700 */
1581 }
1582 .row-actions span:not(:last-child)::after {
1583 content: "";
1584 display: inline-block;
1585 width: 3px;
1586 height: 3px;
1587 border-radius: 50%;
1588 background-color: #d1d5db; /* gray-300 */
1589 margin-left: 8px;
1590 vertical-align: middle;
1591 }
1592
1593 #easy-invoice-confirmation-modal .bg-white {
1594 max-height: 90vh;
1595 overflow-y: auto;
1596 }
1597 #easy-invoice-confirmation-modal .flex.justify-end {
1598 flex-wrap: wrap;
1599 }
1600
1601 .premium-feature .premium-duplicate-btn {
1602 border: 1.5px solid #FFD700;
1603 background: linear-gradient(90deg, #fffbe6 0%, #fff8dc 100%);
1604 box-shadow: 0 0 6px 0 #ffe06633;
1605 position: relative;
1606 transition: box-shadow 0.2s, border-color 0.2s;
1607 }
1608 .premium-feature .premium-duplicate-btn:hover {
1609 box-shadow: 0 0 16px 2px #ffd70099, 0 0 0 2px #fffbe6;
1610 border-color: #FFA500;
1611 animation: shimmer 1.2s linear infinite;
1612 }
1613 @keyframes shimmer {
1614 0% { box-shadow: 0 0 6px 0 #ffe06633; }
1615 50% { box-shadow: 0 0 24px 4px #ffd700cc; }
1616 100% { box-shadow: 0 0 6px 0 #ffe06633; }
1617 }
1618 .premium-feature .premium-tooltip {
1619 opacity: 0;
1620 pointer-events: none;
1621 position: absolute;
1622 left: 50%;
1623 top: 120%;
1624 transform: translateX(-50%);
1625 background: #222;
1626 color: #fffbe6;
1627 padding: 3px 10px;
1628 border-radius: 4px;
1629 font-size: 11px;
1630 white-space: nowrap;
1631 z-index: 10;
1632 transition: opacity 0.2s;
1633 box-shadow: 0 2px 8px #0002;
1634 }
1635 .premium-feature .group:hover .premium-tooltip {
1636 opacity: 1;
1637 }
1638 .premium-feature .premium-duplicate-icon {
1639 filter: drop-shadow(0 0 2px #ffd700);
1640 }
1641
1642 /* Premium Export Button Styling - Minimal */
1643 .premium-export-btn {
1644 border-color: #d1d5db;
1645 transition: all 0.2s ease;
1646 }
1647
1648 .premium-export-btn:hover {
1649 border-color: #9ca3af;
1650 background-color: #f9fafb;
1651 }
1652
1653 .premium-export-btn .crown-icon {
1654 filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.2));
1655 color: #9333ea;
1656 transition: all 0.3s ease;
1657 }
1658
1659 .premium-export-btn:hover .crown-icon {
1660 filter: drop-shadow(0 2px 6px rgba(147, 51, 234, 0.3));
1661 transform: scale(1.1);
1662 color: #7c3aed;
1663 }
1664 </style>
1665