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