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

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

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