| @@ -5,8 +5,11 @@ | ||
| 5 | 5 | * @package EasyInvoice |
| 6 | 6 | * @since 1.0.0 |
| 7 | 7 | */ |
| 8 | 8 | |
| 9 | +if ( ! defined( 'ABSPATH' ) ) { | |
| 10 | + exit; | |
| 11 | +} | |
| 9 | 12 | // Initialize variables |
| 10 | 13 | $quotes = $quotes ?? []; |
| 11 | 14 | $stats = $stats ?? [ |
| 12 | 15 | 'total' => 0, |
| @@ -22,8 +25,12 @@ | ||
| 22 | 25 | // Get current view and status filter |
| 23 | 26 | $view = isset($_GET['view']) ? sanitize_text_field($_GET['view']) : 'all'; |
| 24 | 27 | $status_filter = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : ''; |
| 25 | 28 | |
| 29 | +// Get client filter (from template data, falls back to GET) | |
| 30 | +$client_filter = isset($client_filter) ? (int) $client_filter : (isset($_GET['client_id']) ? absint($_GET['client_id']) : 0); | |
| 31 | +$clients_list = isset($clients_list) && is_array($clients_list) ? $clients_list : []; | |
| 32 | + | |
| 26 | 33 | // Define status filters |
| 27 | 34 | $status_filters = array( |
| 28 | 35 | 'available' => 'Available', |
| 29 | 36 | 'draft' => 'Draft', |
| @@ -53,77 +60,96 @@ | ||
| 53 | 60 | ); |
| 54 | 61 | } |
| 55 | 62 | } |
| 56 | 63 | |
| 57 | -// Group quotes by currency for proper total calculation | |
| 64 | +// Store-wide figures for the header cards. Counting the rows on screen gave | |
| 65 | +// "Total Quotes 20" on a 10,000-quote store; the controller passes the real | |
| 66 | +// per-status counts, and the value by currency is one SQL pass over the | |
| 67 | +// stored totals (accepted + sent + draft, i.e. quotes still in play or won). | |
| 58 | 68 | $currency_totals = []; |
| 59 | -$total_quotes = count($formatted_quotes); | |
| 69 | +$total_quotes = (int) ($draft_count ?? 0) + (int) ($available_count ?? 0) + (int) ($sent_count ?? 0) + (int) ($accepted_count ?? 0) + (int) ($declined_count ?? 0) + (int) ($expired_count ?? 0) + (int) ($cancelled_count ?? 0); | |
| 60 | 70 | |
| 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 | - } | |
| 71 | +foreach (\EasyInvoice\Services\QuoteTotals::valueByCurrency() as $currency_code => $row) { | |
| 72 | + $currency_totals[$currency_code] = [ | |
| 73 | + 'total' => (float) $row['amount'], | |
| 74 | + 'quotes' => (int) $row['count'], | |
| 75 | + 'quote_object' => null, | |
| 76 | + 'currency' => $currency_code, | |
| 77 | + ]; | |
| 77 | 78 | } |
| 78 | 79 | |
| 79 | 80 | $enhanced_stats = [ |
| 80 | - 'total_quotes' => $total_quotes, | |
| 81 | + 'total_quotes' => $total_quotes, | |
| 81 | 82 | '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 | - })) | |
| 83 | + 'accepted_quotes' => (int) ($accepted_count ?? 0), | |
| 84 | + 'pending_quotes' => (int) ($sent_count ?? 0) + (int) ($draft_count ?? 0) + (int) ($available_count ?? 0), | |
| 85 | + 'rejected_quotes' => (int) ($declined_count ?? 0), | |
| 91 | 86 | ]; |
| 92 | 87 | |
| 93 | 88 | ?> |
| 94 | 89 | <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;"> | |
| 90 | + <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;"> | |
| 97 | 91 | <div class="flex items-center justify-between"> |
| 98 | 92 | <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> | |
| 93 | + <h1 class="text-2xl font-bold text-gray-900"><?php esc_html_e('Quotes', 'easy-invoice'); ?></h1> | |
| 94 | + <p class="mt-1 text-sm text-gray-500"><?php esc_html_e('Manage and track your quotes', 'easy-invoice'); ?></p> | |
| 101 | 95 | </div> |
| 102 | 96 | <div class="flex items-center space-x-3"> |
| 103 | 97 | <?php if (isset($_GET['rewrite_flushed'])): ?> |
| 104 | 98 | <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'); ?> | |
| 99 | + <?php esc_html_e('Rewrite rules flushed successfully!', 'easy-invoice'); ?> | |
| 106 | 100 | </div> |
| 107 | 101 | <?php endif; ?> |
| 108 | 102 | |
| 109 | 103 | <?php if (isset($_GET['post_types_registered'])): ?> |
| 110 | 104 | <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'); ?> | |
| 105 | + <?php esc_html_e('Post types registered successfully!', 'easy-invoice'); ?> | |
| 112 | 106 | </div> |
| 113 | 107 | <?php endif; ?> |
| 114 | 108 | |
| 115 | - <a href="<?php echo wp_nonce_url(admin_url('admin-post.php?action=flush_easy_invoice_rewrite_rules'), 'flush_easy_invoice_rewrite_rules'); ?>" | |
| 109 | + <?php | |
| 110 | + // "Flush Rewrite Rules" is a troubleshooting action, not something a | |
| 111 | + // user does as part of their job — but it rendered unconditionally as | |
| 112 | + // a prominent button right beside the primary "Create New Quote" CTA | |
| 113 | + // on every admin's screen. It is now hidden by default and shown when | |
| 114 | + // WP_DEBUG is on, or when a site opts in via the filter. | |
| 115 | + // | |
| 116 | + // Kept rather than deleted because it is the manual escape hatch if | |
| 117 | + // quote permalinks ever 404. | |
| 118 | + $ei_show_maintenance_tools = apply_filters( | |
| 119 | + 'easy_invoice_show_maintenance_tools', | |
| 120 | + defined('WP_DEBUG') && WP_DEBUG | |
| 121 | + ); | |
| 122 | + if ($ei_show_maintenance_tools) : ?> | |
| 123 | + <a href="<?php echo esc_url(wp_nonce_url(admin_url('admin-post.php?action=flush_easy_invoice_rewrite_rules'), 'flush_easy_invoice_rewrite_rules')); ?>" | |
| 116 | 124 | 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'); ?> | |
| 125 | + <?php esc_html_e('Flush Rewrite Rules', 'easy-invoice'); ?> | |
| 118 | 126 | </a> |
| 127 | + <?php endif; ?> | |
| 119 | 128 | |
| 120 | - <?php if (!easy_invoice_has_pro()): ?> | |
| 129 | + <?php | |
| 130 | + // "Export All" is part of the bulk_operations addon | |
| 131 | + // (Personal tier). See templates/invoices/listing.php | |
| 132 | + // for the canonical three-state gate pattern. | |
| 133 | + $ei_bulk_ops_loaded = \EasyInvoice\Addons\AddonManager::shouldLoad('bulk_operations'); | |
| 134 | + ?> | |
| 135 | + <?php if ($ei_bulk_ops_loaded): ?> | |
| 136 | + <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline;"> | |
| 137 | + <input type="hidden" name="action" value="easy_invoice_export_all_quotes" /> | |
| 138 | + <input type="hidden" name="_wpnonce" value="<?php echo esc_attr(wp_create_nonce('easy_invoice_export_all_quotes')); ?>" /> | |
| 139 | + <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"> | |
| 140 | + <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"> | |
| 141 | + <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> | |
| 142 | + </svg> | |
| 143 | + <?php esc_html_e('Export All', 'easy-invoice'); ?> | |
| 144 | + </button> | |
| 145 | + </form> | |
| 146 | + <?php elseif (!easy_invoice_has_pro()): ?> | |
| 121 | 147 | <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 | 148 | <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 | 149 | <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 | 150 | </svg> |
| 125 | - <?php _e('Export All', 'easy-invoice'); ?> | |
| 151 | + <?php esc_html_e('Export All', 'easy-invoice'); ?> | |
| 126 | 152 | <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 | 153 | <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 | 154 | </svg> |
| 129 | 155 | </button> |
| @@ -130,45 +156,33 @@ | ||
| 130 | 156 | <script> |
| 131 | 157 | jQuery(document).ready(function($) { |
| 132 | 158 | $('#export-all-quotes-btn').on('click', function(e) { |
| 133 | 159 | e.preventDefault(); |
| 134 | - // Show premium popup using the proper confirmation modal | |
| 135 | 160 | if (typeof EasyInvoiceConfirmation !== 'undefined') { |
| 136 | 161 | EasyInvoiceConfirmation.showFeatureUpgrade('Export All Quotes', 'Export all your quotes to CSV, Excel, or PDF format for backup, analysis, or sharing with your team.'); |
| 137 | 162 | } else { |
| 138 | - // Fallback to alert if confirmation modal is not available | |
| 139 | 163 | alert('<?php echo esc_js(__('Exporting all quotes is a premium feature. Upgrade to Easy Invoice Pro to unlock this feature.', 'easy-invoice')); ?>'); |
| 140 | 164 | } |
| 141 | 165 | }); |
| 142 | 166 | }); |
| 143 | 167 | </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 | 168 | <?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"> | |
| 169 | + <?php if (easy_invoice_user_can('ei_create_quote')) : ?> | |
| 170 | +<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 | 171 | <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 | 172 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path> |
| 159 | 173 | </svg> |
| 160 | - <?php _e('Create New Quote', 'easy-invoice'); ?> | |
| 174 | + <?php esc_html_e('Create New Quote', 'easy-invoice'); ?> | |
| 161 | 175 | </button> |
| 176 | +<?php endif; ?> | |
| 162 | 177 | </div> |
| 163 | 178 | </div> |
| 164 | 179 | </div> |
| 165 | 180 | |
| 166 | - <!-- New Quote Modal --> | |
| 167 | 181 | <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 | 182 | <div class="relative mx-auto p-6 border w-96 shadow-xl rounded-lg bg-white my-20"> |
| 169 | 183 | <div class="flex justify-between items-center mb-6"> |
| 170 | - <h3 class="text-xl font-semibold text-gray-900">Create New Quote</h3> | |
| 184 | + <h3 class="text-xl font-semibold text-gray-900"><?php esc_html_e('Create New Quote', 'easy-invoice'); ?></h3> | |
| 171 | 185 | <button type="button" id="close-new-quote-modal" class="text-gray-400 hover:text-gray-600 transition-colors duration-200"> |
| 172 | 186 | <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 173 | 187 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> |
| 174 | 188 | </svg> |
| @@ -183,9 +197,9 @@ | ||
| 183 | 197 | id="new_quote_title" |
| 184 | 198 | required |
| 185 | 199 | placeholder="Enter quote title..." |
| 186 | 200 | 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> | |
| 201 | + <p class="mt-2 text-sm text-gray-500"><?php esc_html_e('This will be the title of your quote', 'easy-invoice'); ?></p> | |
| 188 | 202 | </div> |
| 189 | 203 | |
| 190 | 204 | <div class="flex justify-end space-x-3 pt-4"> |
| 191 | 205 | <button type="button" id="cancel-new-quote" |
| @@ -202,9 +216,8 @@ | ||
| 202 | 216 | <div id="new-quote-message" class="mt-4 hidden"></div> |
| 203 | 217 | </div> |
| 204 | 218 | </div> |
| 205 | 219 | |
| 206 | - <!-- Logs Modal --> | |
| 207 | 220 | <div id="logs-modal" class="fixed z-50 inset-0 overflow-y-auto hidden"> |
| 208 | 221 | <div class="flex items-center justify-center min-h-screen px-4"> |
| 209 | 222 | <div class="fixed inset-0 transition-opacity" aria-hidden="true"> |
| 210 | 223 | <div class="absolute inset-0 bg-gray-500 opacity-75"></div> |
| @@ -211,11 +224,11 @@ | ||
| 211 | 224 | </div> |
| 212 | 225 | <div class="bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:max-w-2xl w-full z-50"> |
| 213 | 226 | <div class="px-6 py-4 border-b border-gray-200"> |
| 214 | 227 | <div class="flex justify-between items-center"> |
| 215 | - <h3 class="text-lg font-medium text-gray-900">Quote Activity Logs</h3> | |
| 228 | + <h3 class="text-lg font-medium text-gray-900"><?php esc_html_e('Quote Activity Logs', 'easy-invoice'); ?></h3> | |
| 216 | 229 | <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> | |
| 230 | + <span class="sr-only"><?php esc_html_e('Close', 'easy-invoice'); ?></span> | |
| 218 | 231 | <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> |
| 219 | 232 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> |
| 220 | 233 | </svg> |
| 221 | 234 | </button> |
| @@ -221,15 +234,13 @@ | ||
| 221 | 234 | </button> |
| 222 | 235 | </div> |
| 223 | 236 | </div> |
| 224 | 237 | <div class="px-6 py-4 modal-body"> |
| 225 | - <!-- Logs content will be loaded here --> | |
| 226 | 238 | </div> |
| 227 | 239 | </div> |
| 228 | 240 | </div> |
| 229 | 241 | </div> |
| 230 | 242 | |
| 231 | - <!-- Stats Cards --> | |
| 232 | 243 | <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8 mt-8"> |
| 233 | 244 | <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1"> |
| 234 | 245 | <div class="flex items-center"> |
| 235 | 246 | <div class="rounded-full bg-indigo-100 p-3 mr-4"> |
| @@ -237,10 +248,10 @@ | ||
| 237 | 248 | <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 | 249 | </svg> |
| 239 | 250 | </div> |
| 240 | 251 | <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> | |
| 252 | + <h3 class="text-sm font-medium text-gray-500"><?php esc_html_e('Total Quotes', 'easy-invoice'); ?></h3> | |
| 253 | + <p class="mt-1 text-2xl font-bold text-gray-900"><?php echo esc_html($enhanced_stats['total_quotes']); ?></p> | |
| 243 | 254 | </div> |
| 244 | 255 | </div> |
| 245 | 256 | </div> |
| 246 | 257 | <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1"> |
| @@ -250,9 +261,9 @@ | ||
| 250 | 261 | <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 | 262 | </svg> |
| 252 | 263 | </div> |
| 253 | 264 | <div> |
| 254 | - <h3 class="text-sm font-medium text-gray-500">Total Value</h3> | |
| 265 | + <h3 class="text-sm font-medium text-gray-500"><?php esc_html_e('Total Value', 'easy-invoice'); ?></h3> | |
| 255 | 266 | <?php if (empty($enhanced_stats['currency_totals'])): ?> |
| 256 | 267 | <p class="mt-1 text-2xl font-bold text-gray-900">$0.00</p> |
| 257 | 268 | <?php else: ?> |
| 258 | 269 | <div class="mt-1 space-y-1"> |
| @@ -257,14 +268,13 @@ | ||
| 257 | 268 | <?php else: ?> |
| 258 | 269 | <div class="mt-1 space-y-1"> |
| 259 | 270 | <?php foreach ($enhanced_stats['currency_totals'] as $currency_code => $currency_data): ?> |
| 260 | 271 | <?php |
| 261 | - $formatter = new \EasyInvoice\Helpers\QuoteFormatter($currency_data['quote_object']); | |
| 262 | - $formatted_amount = $formatter->format($currency_data['total']); | |
| 272 | + $formatted_amount = easy_invoice_format_money((float) $currency_data['total'], null, (string) $currency_code); | |
| 263 | 273 | ?> |
| 264 | 274 | <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> | |
| 275 | + <span class="text-lg font-bold text-gray-900"><?php echo esc_html($formatted_amount); ?></span> | |
| 276 | + <span class="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded"><?php echo esc_html($currency_data['quotes']); ?> quote<?php echo $currency_data['quotes'] > 1 ? 's' : ''; ?></span> | |
| 267 | 277 | </div> |
| 268 | 278 | <?php endforeach; ?> |
| 269 | 279 | </div> |
| 270 | 280 | <?php endif; ?> |
| @@ -278,10 +288,10 @@ | ||
| 278 | 288 | <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 | 289 | </svg> |
| 280 | 290 | </div> |
| 281 | 291 | <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> | |
| 292 | + <h3 class="text-sm font-medium text-gray-500"><?php esc_html_e('Accepted Quotes', 'easy-invoice'); ?></h3> | |
| 293 | + <p class="mt-1 text-2xl font-bold text-green-600"><?php echo esc_html($enhanced_stats['accepted_quotes']); ?></p> | |
| 284 | 294 | </div> |
| 285 | 295 | </div> |
| 286 | 296 | </div> |
| 287 | 297 | <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1"> |
| @@ -291,16 +301,15 @@ | ||
| 291 | 301 | <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 | 302 | </svg> |
| 293 | 303 | </div> |
| 294 | 304 | <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> | |
| 305 | + <h3 class="text-sm font-medium text-gray-500"><?php esc_html_e('Pending Quotes', 'easy-invoice'); ?></h3> | |
| 306 | + <p class="mt-1 text-2xl font-bold text-yellow-600"><?php echo esc_html($enhanced_stats['pending_quotes']); ?></p> | |
| 297 | 307 | </div> |
| 298 | 308 | </div> |
| 299 | 309 | </div> |
| 300 | 310 | </div> |
| 301 | 311 | |
| 302 | - <!-- Top Pagination --> | |
| 303 | 312 | <?php if (isset($pagination) && $pagination['total_pages'] > 1): ?> |
| 304 | 313 | <div class="mb-4"> |
| 305 | 314 | <div class="px-4 py-3 flex items-center justify-between"> |
| 306 | 315 | <div class="flex items-center space-x-2"> |
| @@ -305,18 +314,18 @@ | ||
| 305 | 314 | <div class="px-4 py-3 flex items-center justify-between"> |
| 306 | 315 | <div class="flex items-center space-x-2"> |
| 307 | 316 | <span class="text-sm text-gray-700"> |
| 308 | 317 | Showing |
| 309 | - <span class="font-medium"><?php echo (($pagination['current_page'] - 1) * $pagination['per_page']) + 1; ?></span> | |
| 318 | + <span class="font-medium"><?php echo (int) ((($pagination['current_page'] - 1) * $pagination['per_page']) + 1); ?></span> | |
| 310 | 319 | to |
| 311 | - <span class="font-medium"><?php echo min($pagination['current_page'] * $pagination['per_page'], $pagination['total_quotes']); ?></span> | |
| 320 | + <span class="font-medium"><?php echo (int) min($pagination['current_page'] * $pagination['per_page'], $pagination['total_quotes']); ?></span> | |
| 312 | 321 | of |
| 313 | - <span class="font-medium"><?php echo $pagination['total_quotes']; ?></span> | |
| 322 | + <span class="font-medium"><?php echo esc_html($pagination['total_quotes']); ?></span> | |
| 314 | 323 | results |
| 315 | 324 | </span> |
| 316 | 325 | </div> |
| 317 | 326 | <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> | |
| 327 | + <span class="text-sm text-gray-500">Page <?php echo esc_html($pagination['current_page']); ?> of <?php echo esc_html($pagination['total_pages']); ?></span> | |
| 319 | 328 | <div class="flex space-x-1"> |
| 320 | 329 | <?php |
| 321 | 330 | $prev_page = $pagination['current_page'] > 1 ? $pagination['current_page'] - 1 : 1; |
| 322 | 331 | $next_page = $pagination['current_page'] < $pagination['total_pages'] ? $pagination['current_page'] + 1 : $pagination['total_pages']; |
| @@ -328,11 +337,11 @@ | ||
| 328 | 337 | <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 | 338 | <?php endif; |
| 330 | 339 | for ($i = $start_page; $i <= $end_page; $i++): |
| 331 | 340 | 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> | |
| 341 | + <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 esc_html($i); ?></span> | |
| 333 | 342 | <?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> | |
| 343 | + <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 esc_html($i); ?></a> | |
| 335 | 344 | <?php endif; |
| 336 | 345 | endfor; |
| 337 | 346 | if ($pagination['current_page'] < $pagination['total_pages']): ?> |
| 338 | 347 | <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> |
| @@ -342,11 +351,14 @@ | ||
| 342 | 351 | </div> |
| 343 | 352 | </div> |
| 344 | 353 | <?php endif; ?> |
| 345 | 354 | |
| 346 | - <!-- Main Content Area --> | |
| 347 | - <div class="bg-white shadow rounded-lg"> | |
| 348 | - <!-- Navigation Tabs --> | |
| 355 | + <?php // `overflow-hidden` matters here, it is not just for rounding the corners. | |
| 356 | + // The quotes table is wider than this card (9 columns vs the invoice list's 6) | |
| 357 | + // and scrolls inside its own `overflow-x-auto` wrapper. Without this, that | |
| 358 | + // overflow escaped every ancestor and gave the whole admin page a horizontal | |
| 359 | + // scrollbar. The invoice listing has always had it — see invoices/listing.php. ?> | |
| 360 | + <div class="bg-white shadow rounded-lg overflow-hidden"> | |
| 349 | 361 | <div class="border-b border-gray-200 bg-gradient-to-r from-indigo-50 to-white"> |
| 350 | 362 | <nav class="flex"> |
| 351 | 363 | <?php |
| 352 | 364 | // Use the counts passed from the controller |
| @@ -380,9 +392,9 @@ | ||
| 380 | 392 | ? 'border-b-2 border-indigo-500 text-indigo-600 bg-white' |
| 381 | 393 | : 'border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'; |
| 382 | 394 | ?> |
| 383 | 395 | <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; ?>"> | |
| 396 | + class="flex items-center whitespace-nowrap py-4 px-6 font-medium text-sm transition-colors duration-200 <?php echo esc_attr($class); ?>"> | |
| 385 | 397 | <?php if ($tab_key === 'all'): ?> |
| 386 | 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"> |
| 387 | 399 | <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 | 400 | </svg> |
| @@ -414,36 +426,35 @@ | ||
| 414 | 426 | <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 | 427 | <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 | 428 | </svg> |
| 417 | 429 | <?php endif; ?> |
| 418 | - <?php echo $tab_label; ?> (<?php echo $count; ?>) | |
| 430 | + <?php echo esc_html($tab_label); ?> (<?php echo esc_html($count); ?>) | |
| 419 | 431 | </a> |
| 420 | 432 | <?php endforeach; ?> |
| 421 | 433 | </nav> |
| 422 | 434 | </div> |
| 423 | 435 | |
| 424 | - <!-- Filters and Bulk Actions with Search --> | |
| 425 | 436 | <div class="p-4 flex flex-wrap items-center justify-between bg-white border-b border-gray-100"> |
| 426 | - <!-- Bulk Actions (Left Side) --> | |
| 427 | 437 | <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'); ?>"> | |
| 438 | + <?php if (easy_invoice_user_can('ei_create_quote')) : ?> | |
| 439 | +<form id="bulk-action-form" method="post" class="flex items-center gap-2"> | |
| 440 | + <input type="hidden" name="nonce" value="<?php echo esc_attr(wp_create_nonce('easy_invoice_admin_nonce')); ?>"> | |
| 430 | 441 | |
| 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> | |
| 442 | + <select name="bulk_action" aria-label="Bulk action for selected quotes" 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"> | |
| 443 | + <option value=""><?php esc_html_e('Bulk Actions', 'easy-invoice'); ?></option> | |
| 433 | 444 | <?php if ($view === 'cancelled'): ?> |
| 434 | - <option value="restore">Restore Selected</option> | |
| 435 | - <option value="delete">Delete Permanently</option> | |
| 445 | + <option value="restore"><?php esc_html_e('Restore Selected', 'easy-invoice'); ?></option> | |
| 446 | + <option value="delete"><?php esc_html_e('Delete Permanently', 'easy-invoice'); ?></option> | |
| 436 | 447 | <?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> | |
| 448 | + <option value="delete"><?php esc_html_e('Delete Selected', 'easy-invoice'); ?></option> | |
| 449 | + <option value="trash"><?php esc_html_e('Move to Trash', 'easy-invoice'); ?></option> | |
| 450 | + <option value="draft"><?php esc_html_e('Move to Draft', 'easy-invoice'); ?></option> | |
| 440 | 451 | <?php endif; ?> |
| 441 | - <option value="export">Export Selected</option> | |
| 452 | + <option value="export" data-requires-pro="1"><?php esc_html_e('Export Selected (Pro)', 'easy-invoice'); ?></option> | |
| 442 | 453 | </select> |
| 443 | 454 | |
| 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 | |
| 455 | + <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"> | |
| 456 | + Apply | |
| 446 | 457 | </button> |
| 447 | 458 | |
| 448 | 459 | <?php if ($view === 'cancelled' && !empty($quotes)): ?> |
| 449 | 460 | <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"> |
| @@ -453,19 +464,32 @@ | ||
| 453 | 464 | Empty Trash |
| 454 | 465 | </button> |
| 455 | 466 | <?php endif; ?> |
| 456 | 467 | </form> |
| 468 | +<?php endif; ?> | |
| 457 | 469 | </div> |
| 458 | 470 | |
| 459 | - <!-- Search Form (Right Side) --> | |
| 460 | 471 | <div class="flex items-center gap-2"> |
| 461 | 472 | <form method="get" class="flex items-center gap-2"> |
| 462 | - <input type="hidden" name="page" value="easy-invoice-quotes-all"> | |
| 473 | + <input type="hidden" name="page" value="easy-quote-all"> | |
| 463 | 474 | <input type="hidden" name="view" value="<?php echo esc_attr($view); ?>"> |
| 464 | 475 | <?php if (!empty($status_filter)): ?> |
| 465 | 476 | <input type="hidden" name="status" value="<?php echo esc_attr($status_filter); ?>"> |
| 466 | 477 | <?php endif; ?> |
| 467 | 478 | |
| 479 | + <?php if (!empty($clients_list)): ?> | |
| 480 | + <select name="client_id" aria-label="<?php esc_attr_e('Filter by client', 'easy-invoice'); ?>" | |
| 481 | + 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" | |
| 482 | + onchange="this.form.submit()"> | |
| 483 | + <option value=""><?php esc_html_e('All clients', 'easy-invoice'); ?></option> | |
| 484 | + <?php foreach ($clients_list as $client_option): ?> | |
| 485 | + <option value="<?php echo esc_attr($client_option['id']); ?>" <?php selected((int) $client_filter, (int) $client_option['id']); ?>> | |
| 486 | + <?php echo esc_html($client_option['name']); ?> | |
| 487 | + </option> | |
| 488 | + <?php endforeach; ?> | |
| 489 | + </select> | |
| 490 | + <?php endif; ?> | |
| 491 | + | |
| 468 | 492 | <div class="relative"> |
| 469 | 493 | <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> |
| 470 | 494 | <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 | 495 | <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> |
| @@ -473,19 +497,19 @@ | ||
| 473 | 497 | </div> |
| 474 | 498 | <input type="text" |
| 475 | 499 | name="search" |
| 476 | 500 | value="<?php echo esc_attr($search_query); ?>" |
| 477 | - placeholder="<?php _e('Search quotes, numbers, or clients...', 'easy-invoice'); ?>" | |
| 501 | + placeholder="<?php echo esc_js(__('Search quotes, numbers, or clients...', 'easy-invoice')); ?>" | |
| 478 | 502 | 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 | 503 | </div> |
| 480 | 504 | |
| 481 | 505 | <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'); ?> | |
| 506 | + <?php esc_html_e('Search', 'easy-invoice'); ?> | |
| 483 | 507 | </button> |
| 484 | 508 | |
| 485 | 509 | <?php if (!empty($search_query)): ?> |
| 486 | 510 | <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'); ?> | |
| 511 | + <?php esc_html_e('Clear', 'easy-invoice'); ?> | |
| 488 | 512 | </a> |
| 489 | 513 | <?php endif; ?> |
| 490 | 514 | </form> |
| 491 | 515 | </div> |
| @@ -490,15 +514,14 @@ | ||
| 490 | 514 | </form> |
| 491 | 515 | </div> |
| 492 | 516 | </div> |
| 493 | 517 | |
| 494 | - <!-- Quotes Table --> | |
| 495 | 518 | <div class="overflow-x-auto"> |
| 496 | - <table class="min-w-full divide-y divide-gray-200"> | |
| 519 | + <table class="min-w-full divide-y divide-gray-200 ei-quotes-table"> | |
| 497 | 520 | <thead class="bg-gray-50"> |
| 498 | 521 | <tr> |
| 499 | 522 | <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"> | |
| 523 | + <input type="checkbox" id="select-all" aria-label="Select all quotes on this page" 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 | 524 | </th> |
| 502 | 525 | <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> |
| 503 | 526 | Quote |
| 504 | 527 | </th> |
| @@ -520,9 +543,9 @@ | ||
| 520 | 543 | <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> |
| 521 | 544 | Logs |
| 522 | 545 | </th> |
| 523 | 546 | <th scope="col" class="relative px-6 py-3"> |
| 524 | - <span class="sr-only">Actions</span> | |
| 547 | + <span class="sr-only"><?php esc_html_e('Actions', 'easy-invoice'); ?></span> | |
| 525 | 548 | </th> |
| 526 | 549 | </tr> |
| 527 | 550 | </thead> |
| 528 | 551 | <tbody class="bg-white divide-y divide-gray-200"> |
| @@ -532,10 +555,10 @@ | ||
| 532 | 555 | <div class="flex flex-col items-center"> |
| 533 | 556 | <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 | 557 | <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 | 558 | </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> | |
| 559 | + <h3 class="text-lg font-medium text-gray-900 mb-2"><?php esc_html_e('No quotes found', 'easy-invoice'); ?></h3> | |
| 560 | + <p class="text-gray-500 mb-4"><?php esc_html_e('Get started by creating your first quote.', 'easy-invoice'); ?></p> | |
| 538 | 561 | <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 | 562 | <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 | 563 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path> |
| 541 | 564 | </svg> |
| @@ -602,9 +625,9 @@ | ||
| 602 | 625 | $quote->ensureProperSlug(); |
| 603 | 626 | ?> |
| 604 | 627 | <tr class="hover:bg-gray-50 transition-colors duration-150"> |
| 605 | 628 | <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(); ?>"> | |
| 629 | + <input type="checkbox" aria-label="<?php /* translators: %s: document number. */ echo esc_attr(sprintf(__('Select quote %s', 'easy-invoice'), $quote->getNumber())); ?>" 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 (int) $quote->getId(); ?>"> | |
| 607 | 630 | </td> |
| 608 | 631 | <td class="px-6 py-4 whitespace-nowrap"> |
| 609 | 632 | <div class="flex items-center"> |
| 610 | 633 | <div class="flex-shrink-0 h-10 w-10"> |
| @@ -615,30 +638,36 @@ | ||
| 615 | 638 | </div> |
| 616 | 639 | </div> |
| 617 | 640 | <div class="ml-4"> |
| 618 | 641 | <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"> | |
| 642 | + <a href="<?php echo esc_url(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 | 643 | <?php echo esc_html($quote->getTitle() ?: 'Untitled Quote'); ?> |
| 621 | 644 | </a> |
| 622 | 645 | </div> |
| 623 | 646 | <div class="text-sm text-gray-500"> |
| 624 | 647 | <?php echo esc_html($quote->getNumber() ?: 'QT-' . $quote->getId()); ?> |
| 648 | + <?php if (!in_array((string) $quote->getStatus(), ['draft'], true)) : ?> | |
| 649 | + <span class="ei-viewed ml-2 inline-flex items-center gap-1 <?php echo \EasyInvoice\Services\DocumentViews::summary((int) $quote->getId())['count'] ? 'text-emerald-600' : 'text-gray-400'; ?>"> | |
| 650 | + <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><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"/></svg> | |
| 651 | + <?php echo esc_html(\EasyInvoice\Services\DocumentViews::label((int) $quote->getId())); ?> | |
| 652 | + </span> | |
| 653 | + <?php endif; ?> | |
| 625 | 654 | </div> |
| 626 | 655 | <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> | |
| 656 | + <span class="view"><a href="<?php echo esc_url(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> | |
| 657 | + <span class="edit"><a href="<?php echo esc_url(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 | 658 | <?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> | |
| 659 | + <span class="restore"><a href="#" class="restore-quote text-green-600" data-quote-id="<?php echo (int) $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> | |
| 660 | + <span class="delete"><a href="#" class="delete-quote-btn text-red-600" data-quote-id="<?php echo (int) $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 | 661 | <?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> | |
| 662 | + <span class="trash"><a href="#" class="trash-quote text-red-600" data-quote-id="<?php echo (int) $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> | |
| 663 | + <span class="email"><a href="#" class="email-quote" data-quote-id="<?php echo (int) $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> | |
| 664 | + <span class="pdf"><a href="<?php echo esc_url(get_permalink($quote->getId())); ?>?auto_download_pdf=1" target="_blank" class="download-pdf-quote" data-quote-id="<?php echo (int) $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 | 665 | <?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> | |
| 666 | + <span class="export premium-feature"><a href="#" class="export-quote-pro-placeholder text-yellow-600 font-semibold" data-quote-id="<?php echo (int) $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 | 667 | <?php endif; ?> |
| 639 | 668 | <?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> | |
| 669 | + <span class="duplicate premium-feature"><a href="#" class="duplicate-quote-pro-placeholder text-yellow-600 font-semibold" data-quote-id="<?php echo (int) $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 | 670 | <?php endif; ?> |
| 642 | 671 | |
| 643 | 672 | <?php |
| 644 | 673 | // Apply filter for Pro plugin to add secure link actions |
| @@ -643,9 +672,9 @@ | ||
| 643 | 672 | <?php |
| 644 | 673 | // Apply filter for Pro plugin to add secure link actions |
| 645 | 674 | $secure_actions = apply_filters('easy_invoice_quote_row_actions', [], $quote); |
| 646 | 675 | foreach ($secure_actions as $action_key => $action_html) { |
| 647 | - echo '<span class="' . esc_attr($action_key) . '">' . $action_html . '</span>'; | |
| 676 | + echo '<span class="' . esc_attr($action_key) . '">' . wp_kses_post($action_html) . '</span>'; | |
| 648 | 677 | } |
| 649 | 678 | ?> |
| 650 | 679 | <?php endif; ?> |
| 651 | 680 | </div> |
| @@ -683,10 +712,17 @@ | ||
| 683 | 712 | </td> |
| 684 | 713 | <td class="px-6 py-4 whitespace-nowrap"> |
| 685 | 714 | <?php |
| 686 | 715 | $status = $quote->getStatus() ?: 'draft'; |
| 716 | + // An open quote past its expiry date reads as expired even | |
| 717 | + // before the daily expiry check has flipped its status. | |
| 718 | + if (in_array($status, ['available', 'sent'], true) && $quote->getExpiryDate() | |
| 719 | + && strtotime($quote->getExpiryDate()) && gmdate('Y-m-d', strtotime($quote->getExpiryDate())) < gmdate('Y-m-d', current_time('timestamp'))) { | |
| 720 | + $status = 'expired'; | |
| 721 | + } | |
| 687 | 722 | $status_classes = [ |
| 688 | 723 | 'draft' => 'bg-gray-100 text-gray-800 ring-1 ring-gray-300', |
| 724 | + 'available' => 'bg-blue-100 text-blue-800 ring-1 ring-blue-300', | |
| 689 | 725 | 'sent' => 'bg-blue-100 text-blue-800 ring-1 ring-blue-300', |
| 690 | 726 | 'accepted' => 'bg-green-100 text-green-800 ring-1 ring-green-300', |
| 691 | 727 | 'declined' => 'bg-red-100 text-red-800 ring-1 ring-red-300', |
| 692 | 728 | 'expired' => 'bg-yellow-100 text-yellow-800 ring-1 ring-yellow-300' |
| @@ -692,17 +728,17 @@ | ||
| 692 | 728 | 'expired' => 'bg-yellow-100 text-yellow-800 ring-1 ring-yellow-300' |
| 693 | 729 | ]; |
| 694 | 730 | $status_class = $status_classes[$status] ?? 'bg-gray-100 text-gray-800 ring-1 ring-gray-300'; |
| 695 | 731 | ?> |
| 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); ?> | |
| 732 | + <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <?php echo esc_attr($status_class); ?>"> | |
| 733 | + <?php echo esc_html(ucfirst($status)); ?> | |
| 698 | 734 | </span> |
| 699 | 735 | </td> |
| 700 | 736 | <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())) : '—'; ?> | |
| 737 | + <?php echo esc_html($quote->getIssueDate() ? gmdate('M j, Y', strtotime($quote->getIssueDate())) : '—'); ?> | |
| 702 | 738 | </td> |
| 703 | 739 | <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())) : '—'; ?> | |
| 740 | + <?php echo esc_html($quote->getExpiryDate() ? gmdate('M j, Y', strtotime($quote->getExpiryDate())) : '—'); ?> | |
| 705 | 741 | </td> |
| 706 | 742 | <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> |
| 707 | 743 | <?php |
| 708 | 744 | // Get all logs for this quote |
| @@ -713,22 +749,21 @@ | ||
| 713 | 749 | |
| 714 | 750 | if ($logs_count > 0): ?> |
| 715 | 751 | <button type="button" |
| 716 | 752 | class="view-logs-btn text-xs text-indigo-600 hover:text-indigo-800 font-medium" |
| 717 | - data-quote-id="<?php echo $quote->getId(); ?>" | |
| 753 | + data-quote-id="<?php echo (int) $quote->getId(); ?>" | |
| 718 | 754 | 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 | |
| 755 | + data-logs-count="<?php echo esc_attr($logs_count); ?>"> | |
| 756 | + View all <?php echo esc_html($logs_count); ?> logs | |
| 721 | 757 | </button> |
| 722 | 758 | <?php else: ?> |
| 723 | - <span class="text-gray-400 text-xs">No activity</span> | |
| 759 | + <span class="text-gray-400 text-xs"><?php esc_html_e('No activity', 'easy-invoice'); ?></span> | |
| 724 | 760 | <?php endif; ?> |
| 725 | 761 | <?php } catch (\Exception $e) { ?> |
| 726 | - <span class="text-gray-400 text-xs">Error loading logs</span> | |
| 762 | + <span class="text-gray-400 text-xs"><?php esc_html_e('Error loading logs', 'easy-invoice'); ?></span> | |
| 727 | 763 | <?php } ?> |
| 728 | 764 | </td> |
| 729 | 765 | <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 | 766 | </td> |
| 732 | 767 | </tr> |
| 733 | 768 | <?php endforeach; ?> |
| 734 | 769 | <?php endif; ?> |
| @@ -736,9 +771,8 @@ | ||
| 736 | 771 | </table> |
| 737 | 772 | </div> |
| 738 | 773 | </div> |
| 739 | 774 | |
| 740 | - <!-- Bottom Pagination --> | |
| 741 | 775 | <?php if (isset($pagination) && $pagination['total_pages'] > 1): ?> |
| 742 | 776 | <div class="mt-4"> |
| 743 | 777 | <div class="px-4 py-3 flex items-center justify-between"> |
| 744 | 778 | <div class="flex items-center space-x-2"> |
| @@ -743,18 +777,18 @@ | ||
| 743 | 777 | <div class="px-4 py-3 flex items-center justify-between"> |
| 744 | 778 | <div class="flex items-center space-x-2"> |
| 745 | 779 | <span class="text-sm text-gray-700"> |
| 746 | 780 | Showing |
| 747 | - <span class="font-medium"><?php echo (($pagination['current_page'] - 1) * $pagination['per_page']) + 1; ?></span> | |
| 781 | + <span class="font-medium"><?php echo (int) ((($pagination['current_page'] - 1) * $pagination['per_page']) + 1); ?></span> | |
| 748 | 782 | to |
| 749 | - <span class="font-medium"><?php echo min($pagination['current_page'] * $pagination['per_page'], $pagination['total_quotes']); ?></span> | |
| 783 | + <span class="font-medium"><?php echo (int) min($pagination['current_page'] * $pagination['per_page'], $pagination['total_quotes']); ?></span> | |
| 750 | 784 | of |
| 751 | - <span class="font-medium"><?php echo $pagination['total_quotes']; ?></span> | |
| 785 | + <span class="font-medium"><?php echo esc_html($pagination['total_quotes']); ?></span> | |
| 752 | 786 | results |
| 753 | 787 | </span> |
| 754 | 788 | </div> |
| 755 | 789 | <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> | |
| 790 | + <span class="text-sm text-gray-500">Page <?php echo esc_html($pagination['current_page']); ?> of <?php echo esc_html($pagination['total_pages']); ?></span> | |
| 757 | 791 | <div class="flex space-x-1"> |
| 758 | 792 | <?php |
| 759 | 793 | $prev_page = $pagination['current_page'] > 1 ? $pagination['current_page'] - 1 : 1; |
| 760 | 794 | $next_page = $pagination['current_page'] < $pagination['total_pages'] ? $pagination['current_page'] + 1 : $pagination['total_pages']; |
| @@ -766,11 +800,11 @@ | ||
| 766 | 800 | <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 | 801 | <?php endif; |
| 768 | 802 | for ($i = $start_page; $i <= $end_page; $i++): |
| 769 | 803 | 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> | |
| 804 | + <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 esc_html($i); ?></span> | |
| 771 | 805 | <?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> | |
| 806 | + <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 esc_html($i); ?></a> | |
| 773 | 807 | <?php endif; |
| 774 | 808 | endfor; |
| 775 | 809 | if ($pagination['current_page'] < $pagination['total_pages']): ?> |
| 776 | 810 | <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> |
| @@ -816,9 +850,9 @@ | ||
| 816 | 850 | var $message = $('#new-quote-message'); |
| 817 | 851 | var title = $('#new_quote_title').val().trim(); |
| 818 | 852 | |
| 819 | 853 | if (!title) { |
| 820 | - $message.removeClass('hidden').html('<div class="text-red-600 text-sm">Please enter a quote title.</div>'); | |
| 854 | + $message.removeClass('hidden').html('<div class="text-red-600 text-sm"><?php esc_html_e('Please enter a quote title.', 'easy-invoice'); ?></div>'); | |
| 821 | 855 | return; |
| 822 | 856 | } |
| 823 | 857 | |
| 824 | 858 | // Show loading state |
| @@ -831,14 +865,14 @@ | ||
| 831 | 865 | dataType: 'json', |
| 832 | 866 | data: { |
| 833 | 867 | action: 'easy_invoice_create_new_quote', |
| 834 | 868 | title: title, |
| 835 | - nonce: '<?php echo wp_create_nonce('easy_invoice_admin_nonce'); ?>' | |
| 869 | + nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_admin_nonce')); ?>' | |
| 836 | 870 | }, |
| 837 | 871 | success: function(response) { |
| 838 | 872 | if (response.success && response.data && response.data.quote_id) { |
| 839 | 873 | // Show success message below the form |
| 840 | - $message.removeClass('hidden').html('<div class="text-green-600 text-sm">Quote created successfully! Redirecting...</div>'); | |
| 874 | + $message.removeClass('hidden').html('<div class="text-green-600 text-sm"><?php esc_html_e('Quote created successfully! Redirecting...', 'easy-invoice'); ?></div>'); | |
| 841 | 875 | |
| 842 | 876 | // Redirect to the builder page with the new quote ID |
| 843 | 877 | setTimeout(function() { |
| 844 | 878 | window.location.href = 'admin.php?page=easy-invoice-quote-builder&id=' + response.data.quote_id; |
| @@ -848,105 +882,14 @@ | ||
| 848 | 882 | $submitBtn.prop('disabled', false).html('Create Quote'); |
| 849 | 883 | } |
| 850 | 884 | }, |
| 851 | 885 | error: function() { |
| 852 | - $message.removeClass('hidden').html('<div class="text-red-600 text-sm">Error connecting to server. Please try again.</div>'); | |
| 886 | + $message.removeClass('hidden').html('<div class="text-red-600 text-sm"><?php esc_html_e('Error connecting to server. Please try again.', 'easy-invoice'); ?></div>'); | |
| 853 | 887 | $submitBtn.prop('disabled', false).html('Create Quote'); |
| 854 | 888 | } |
| 855 | 889 | }); |
| 856 | 890 | }); |
| 857 | 891 | |
| 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 | 892 | // Select all checkboxes |
| 950 | 893 | $('#select-all').on('change', function() { |
| 951 | 894 | $('.quote-checkbox').prop('checked', $(this).is(':checked')); |
| 952 | 895 | updateBulkActionButton(); |
| @@ -1024,9 +967,9 @@ | ||
| 1024 | 967 | data: { |
| 1025 | 968 | action: 'easy_invoice_bulk_quote_action', |
| 1026 | 969 | quote_ids: selectedQuotes, |
| 1027 | 970 | bulk_action: action, |
| 1028 | - nonce: '<?php echo wp_create_nonce('easy_invoice_admin_nonce'); ?>' | |
| 971 | + nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_admin_nonce')); ?>' | |
| 1029 | 972 | }, |
| 1030 | 973 | success: function(response) { |
| 1031 | 974 | if (response.success) { |
| 1032 | 975 | location.reload(); |
| @@ -1067,9 +1010,9 @@ | ||
| 1067 | 1010 | dataType: 'json', |
| 1068 | 1011 | data: { |
| 1069 | 1012 | action: 'easy_invoice_draft_quote', |
| 1070 | 1013 | quote_id: quoteId, |
| 1071 | - nonce: '<?php echo wp_create_nonce('easy_invoice_admin_nonce'); ?>' | |
| 1014 | + nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_admin_nonce')); ?>' | |
| 1072 | 1015 | }, |
| 1073 | 1016 | success: function(response) { |
| 1074 | 1017 | if (response.success) { |
| 1075 | 1018 | location.reload(); |
| @@ -1093,8 +1036,27 @@ | ||
| 1093 | 1036 | }); |
| 1094 | 1037 | }); |
| 1095 | 1038 | }); |
| 1096 | 1039 | |
| 1040 | + // Convert to invoice: a confirmation, then the draft opens in the builder. | |
| 1041 | + $(document).on('click', '.convert-quote', function(e) { | |
| 1042 | + e.preventDefault(); | |
| 1043 | + var quoteId = $(this).data('quote-id'), quoteNumber = $(this).data('quote-number'); | |
| 1044 | + var run = function() { | |
| 1045 | + $.post(ajaxurl, { action: 'easy_invoice_convert_quote', quote_id: quoteId, nonce: '<?php echo esc_js(wp_create_nonce('easy_invoice_admin_nonce')); ?>' }, function(response) { | |
| 1046 | + if (response && response.success) { | |
| 1047 | + if (typeof EasyInvoiceToast !== 'undefined') { EasyInvoiceToast.success(response.data.message); } | |
| 1048 | + window.location.href = response.data.redirect || ('<?php echo esc_url(admin_url('admin.php?page=easy-invoice-builder&invoice_id=')); ?>' + response.data.invoice_id); | |
| 1049 | + } else if (typeof EasyInvoiceToast !== 'undefined') { | |
| 1050 | + EasyInvoiceToast.error((response && response.data && response.data.message) || '<?php echo esc_js(__('The invoice could not be created.', 'easy-invoice')); ?>'); | |
| 1051 | + } | |
| 1052 | + }); | |
| 1053 | + }; | |
| 1054 | + if (typeof EasyInvoiceConfirmation !== 'undefined') { | |
| 1055 | + EasyInvoiceConfirmation.show({ title: '<?php echo esc_js(__('Convert to invoice', 'easy-invoice')); ?>', message: '<?php echo esc_js(__('Create a draft invoice from quote %s? The quote is marked accepted.', 'easy-invoice')); ?>'.replace('%s', quoteNumber), confirmText: '<?php echo esc_js(__('Create invoice', 'easy-invoice')); ?>', onConfirm: run }); | |
| 1056 | + } else if (window.confirm('<?php echo esc_js(__('Create a draft invoice from this quote?', 'easy-invoice')); ?>')) { run(); } | |
| 1057 | + }); | |
| 1058 | + | |
| 1097 | 1059 | $(document).on('click', '.trash-quote', function(e) { |
| 1098 | 1060 | e.preventDefault(); |
| 1099 | 1061 | const quoteId = $(this).data('quote-id'); |
| 1100 | 1062 | const quoteNumber = $(this).data('quote-number'); |
| @@ -1106,9 +1068,9 @@ | ||
| 1106 | 1068 | dataType: 'json', |
| 1107 | 1069 | data: { |
| 1108 | 1070 | action: 'easy_invoice_trash_quote', |
| 1109 | 1071 | quote_id: quoteId, |
| 1110 | - nonce: '<?php echo wp_create_nonce('easy_invoice_admin_nonce'); ?>' | |
| 1072 | + nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_admin_nonce')); ?>' | |
| 1111 | 1073 | }, |
| 1112 | 1074 | success: function(response) { |
| 1113 | 1075 | if (response.success) { |
| 1114 | 1076 | location.reload(); |
| @@ -1152,9 +1114,9 @@ | ||
| 1152 | 1114 | type: "POST", |
| 1153 | 1115 | data: { |
| 1154 | 1116 | action: "easy_invoice_send_quote_email", |
| 1155 | 1117 | quote_id: quoteId, |
| 1156 | - nonce: '<?php echo wp_create_nonce('easy_invoice_send_quote_email'); ?>' | |
| 1118 | + nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_send_quote_email')); ?>' | |
| 1157 | 1119 | }, |
| 1158 | 1120 | success: function(response) { |
| 1159 | 1121 | if (response.success) { |
| 1160 | 1122 | EasyInvoiceToast.success("Email sent successfully!"); |
| @@ -1185,9 +1147,9 @@ | ||
| 1185 | 1147 | type: "POST", |
| 1186 | 1148 | data: { |
| 1187 | 1149 | action: "easy_invoice_send_quote_email", |
| 1188 | 1150 | quote_id: quoteId, |
| 1189 | - nonce: '<?php echo wp_create_nonce('easy_invoice_send_quote_email'); ?>' | |
| 1151 | + nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_send_quote_email')); ?>' | |
| 1190 | 1152 | }, |
| 1191 | 1153 | success: function(response) { |
| 1192 | 1154 | if (response.success) { |
| 1193 | 1155 | EasyInvoiceToast.success("Email sent successfully!"); |
| @@ -1208,46 +1170,23 @@ | ||
| 1208 | 1170 | } |
| 1209 | 1171 | } |
| 1210 | 1172 | }); |
| 1211 | 1173 | |
| 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'); | |
| 1174 | + // Download PDF: let the anchor's native href do the job. | |
| 1175 | + // | |
| 1176 | + // Historically this handler intercepted the click and did an | |
| 1177 | + // admin-ajax round-trip that minted a per-user nonce and opened | |
| 1178 | + // the resulting admin-ajax URL in a new tab. Same failure modes | |
| 1179 | + // as the invoice listing — see that template for the full | |
| 1180 | + // rationale. Removing the interceptor collapses three server | |
| 1181 | + // round-trips into one and sidesteps every failure mode. The | |
| 1182 | + // `easy_invoice_download_quote_pdf` / `easy_invoice_generate_quote_pdf` | |
| 1183 | + // handlers are still registered on the server for any external | |
| 1184 | + // integration that used them (they now have their own robust | |
| 1185 | + // 3-path auth + JS redirect fallback — see | |
| 1186 | + // EasyInvoiceAjax::generateQuotePdf), but this button no longer | |
| 1187 | + // depends on them. | |
| 1216 | 1188 | |
| 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 | 1189 | // Delete quote |
| 1251 | 1190 | $(document).on('click', '.delete-quote-btn', function() { |
| 1252 | 1191 | const quoteId = $(this).data('quote-id'); |
| 1253 | 1192 | const $row = $(this).closest('tr'); |
| @@ -1259,9 +1198,9 @@ | ||
| 1259 | 1198 | dataType: 'json', |
| 1260 | 1199 | data: { |
| 1261 | 1200 | action: 'easy_invoice_delete_quote', |
| 1262 | 1201 | quote_id: quoteId, |
| 1263 | - nonce: '<?php echo wp_create_nonce('easy_invoice_admin_nonce'); ?>' | |
| 1202 | + nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_admin_nonce')); ?>' | |
| 1264 | 1203 | }, |
| 1265 | 1204 | success: function(response) { |
| 1266 | 1205 | if (response.success) { |
| 1267 | 1206 | $row.fadeOut(300, function() { |
| @@ -1339,9 +1278,9 @@ | ||
| 1339 | 1278 | type: 'POST', |
| 1340 | 1279 | dataType: 'json', |
| 1341 | 1280 | data: { |
| 1342 | 1281 | action: 'easy_invoice_empty_trash', |
| 1343 | - nonce: '<?php echo wp_create_nonce('easy_invoice_nonce'); ?>' | |
| 1282 | + nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_nonce')); ?>' | |
| 1344 | 1283 | }, |
| 1345 | 1284 | success: function(response) { |
| 1346 | 1285 | if (response.success) { |
| 1347 | 1286 | EasyInvoiceToast.success('Trash emptied successfully'); |
| @@ -1375,9 +1314,9 @@ | ||
| 1375 | 1314 | dataType: 'json', |
| 1376 | 1315 | data: { |
| 1377 | 1316 | action: 'easy_invoice_restore_quote', |
| 1378 | 1317 | quote_id: quoteId, |
| 1379 | - nonce: '<?php echo wp_create_nonce('easy_invoice_admin_nonce'); ?>' | |
| 1318 | + nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_admin_nonce')); ?>' | |
| 1380 | 1319 | }, |
| 1381 | 1320 | success: function(response) { |
| 1382 | 1321 | if (response.success) { |
| 1383 | 1322 | location.reload(); |
| @@ -1426,9 +1365,9 @@ | ||
| 1426 | 1365 | dataType: 'json', |
| 1427 | 1366 | data: { |
| 1428 | 1367 | action: 'easy_invoice_duplicate_quote', |
| 1429 | 1368 | quote_id: quoteId, |
| 1430 | - nonce: '<?php echo wp_create_nonce('easy_invoice_admin_nonce'); ?>' | |
| 1369 | + nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_admin_nonce')); ?>' | |
| 1431 | 1370 | }, |
| 1432 | 1371 | success: function(response) { |
| 1433 | 1372 | if (response.success && response.data && response.data.quote_id) { |
| 1434 | 1373 | EasyInvoiceConfirmation.show({ |
| @@ -1479,9 +1418,9 @@ | ||
| 1479 | 1418 | dataType: 'json', |
| 1480 | 1419 | data: { |
| 1481 | 1420 | action: 'easy_invoice_get_quote_logs', |
| 1482 | 1421 | quote_id: quoteId, |
| 1483 | - nonce: '<?php echo wp_create_nonce('easy_invoice_admin_nonce'); ?>' | |
| 1422 | + nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_admin_nonce')); ?>' | |
| 1484 | 1423 | }, |
| 1485 | 1424 | success: function(response) { |
| 1486 | 1425 | if (response.success && response.data && response.data.logs) { |
| 1487 | 1426 | let logsHtml = '<div class="space-y-4">'; |
| @@ -1518,13 +1457,13 @@ | ||
| 1518 | 1457 | |
| 1519 | 1458 | logsHtml += '</div>'; |
| 1520 | 1459 | $('#logs-modal .modal-body').html(logsHtml); |
| 1521 | 1460 | } else { |
| 1522 | - $('#logs-modal .modal-body').html('<div class="text-center py-8 text-gray-500">No logs found for this quote.</div>'); | |
| 1461 | + $('#logs-modal .modal-body').html('<div class="text-center py-8 text-gray-500"><?php esc_html_e('No logs found for this quote.', 'easy-invoice'); ?></div>'); | |
| 1523 | 1462 | } |
| 1524 | 1463 | }, |
| 1525 | 1464 | error: function() { |
| 1526 | - $('#logs-modal .modal-body').html('<div class="text-center py-8 text-red-500">Error loading logs. Please try again.</div>'); | |
| 1465 | + $('#logs-modal .modal-body').html('<div class="text-center py-8 text-red-500"><?php esc_html_e('Error loading logs. Please try again.', 'easy-invoice'); ?></div>'); | |
| 1527 | 1466 | } |
| 1528 | 1467 | }); |
| 1529 | 1468 | }); |
| 1530 | 1469 | |
| @@ -1544,17 +1483,47 @@ | ||
| 1544 | 1483 | </script> |
| 1545 | 1484 | |
| 1546 | 1485 | <style> |
| 1547 | 1486 | /* Modern row actions styling (copied from invoice listing) */ |
| 1487 | +/* Team roles: controls a member may not use are not drawn (the endpoints check again). */ | |
| 1488 | +<?php if (!easy_invoice_user_can('ei_create_quote')) : ?>.row-actions .edit, .row-actions .duplicate, .row-actions .convert { display: none; }<?php endif; ?> | |
| 1489 | +<?php if (!easy_invoice_user_can('ei_delete_quote')) : ?>.row-actions .trash, .row-actions .delete, .row-actions .restore { display: none; }<?php endif; ?> | |
| 1490 | +<?php if (!easy_invoice_user_can('ei_send_invoice')) : ?>.row-actions .email { display: none; }<?php endif; ?> | |
| 1491 | +/* Nine columns: the title cell may wrap, and the dates sit closer together. */ | |
| 1492 | +.ei-quotes-table td:nth-child(2) { white-space: normal; } | |
| 1493 | +.ei-quotes-table td:nth-child(2) .ei-viewed { white-space: nowrap; } | |
| 1494 | +.ei-quotes-table th, .ei-quotes-table td { padding-left: 1rem; padding-right: 1rem; } | |
| 1548 | 1495 | .row-actions { |
| 1549 | 1496 | visibility: hidden; |
| 1550 | 1497 | font-size: 12px; |
| 1551 | 1498 | padding-top: 4px; |
| 1552 | 1499 | display: flex; |
| 1553 | - gap: 8px; | |
| 1500 | + flex-wrap: wrap; /* a long action list must not widen the column past the screen */ | |
| 1501 | + gap: 4px 8px; | |
| 1554 | 1502 | } |
| 1555 | -tr:hover .row-actions { | |
| 1503 | +tr:hover .row-actions, | |
| 1504 | +tr:focus-within .row-actions { | |
| 1556 | 1505 | visibility: visible; |
| 1506 | +} | |
| 1507 | + | |
| 1508 | +/* | |
| 1509 | + * Row actions were revealed on `tr:hover` only, which made them unreachable for | |
| 1510 | + * two whole groups of users: | |
| 1511 | + * | |
| 1512 | + * - Keyboard users. `visibility: hidden` also removes elements from the tab | |
| 1513 | + * order, so View / Edit / Trash / Send Email / Download PDF / Duplicate could | |
| 1514 | + * never be focused at all. `:focus-within` above fixes that — tabbing into the | |
| 1515 | + * row now reveals them. | |
| 1516 | + * - Touch users. There is no hover on a phone or tablet, so every row action was | |
| 1517 | + * permanently invisible and there was no other way to reach them. | |
| 1518 | + * | |
| 1519 | + * On pointer-less devices, and on the narrow widths WordPress itself treats as | |
| 1520 | + * mobile admin, the actions are simply always visible. | |
| 1521 | + */ | |
| 1522 | +@media (hover: none), (max-width: 782px) { | |
| 1523 | + .row-actions { | |
| 1524 | + visibility: visible; | |
| 1525 | + } | |
| 1557 | 1526 | } |
| 1558 | 1527 | .row-actions span a { |
| 1559 | 1528 | color: #6366f1; /* indigo-500 */ |
| 1560 | 1529 | text-decoration: none; |