0, 'available' => 0, 'draft' => 0, 'sent' => 0, 'accepted' => 0, 'declined' => 0, 'cancelled' => 0, 'expired' => 0 ]; // Get current view and status filter $view = isset($_GET['view']) ? sanitize_text_field($_GET['view']) : 'all'; $status_filter = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : ''; // Get client filter (from template data, falls back to GET) $client_filter = isset($client_filter) ? (int) $client_filter : (isset($_GET['client_id']) ? absint($_GET['client_id']) : 0); $clients_list = isset($clients_list) && is_array($clients_list) ? $clients_list : []; // Define status filters $status_filters = array( 'available' => 'Available', 'draft' => 'Draft', 'sent' => 'Sent', 'accepted' => 'Accepted', 'declined' => 'Declined', 'cancelled' => 'Trash', 'expired' => 'Expired' ); // Extract quote data for statistics $formatted_quotes = array(); foreach ($quotes as $quote) { // For Quote model objects if (is_object($quote)) { $formatted_quotes[] = array( 'id' => $quote->getId(), 'amount' => $quote->getTotal(), 'status' => $quote->getStatus(), ); } else { // For already formatted quote arrays $formatted_quotes[] = array( 'id' => $quote['id'], 'amount' => isset($quote['total_amount']) ? $quote['total_amount'] : $quote['amount'], 'status' => $quote['status'], ); } } // Store-wide figures for the header cards. Counting the rows on screen gave // "Total Quotes 20" on a 10,000-quote store; the controller passes the real // per-status counts, and the value by currency is one SQL pass over the // stored totals (accepted + sent + draft, i.e. quotes still in play or won). $currency_totals = []; $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); foreach (\EasyInvoice\Services\QuoteTotals::valueByCurrency() as $currency_code => $row) { $currency_totals[$currency_code] = [ 'total' => (float) $row['amount'], 'quotes' => (int) $row['count'], 'quote_object' => null, 'currency' => $currency_code, ]; } $enhanced_stats = [ 'total_quotes' => $total_quotes, 'currency_totals' => $currency_totals, 'accepted_quotes' => (int) ($accepted_count ?? 0), 'pending_quotes' => (int) ($sent_count ?? 0) + (int) ($draft_count ?? 0) + (int) ($available_count ?? 0), 'rejected_quotes' => (int) ($declined_count ?? 0), ]; ?>

$0.00

$currency_data): ?>
quote 1 ? 's' : ''; ?>

1): ?>
Showing to of results
Page of
1 ? $pagination['current_page'] - 1 : 1; $next_page = $pagination['current_page'] < $pagination['total_pages'] ? $pagination['current_page'] + 1 : $pagination['total_pages']; $current_url = add_query_arg($_GET, admin_url('admin.php')); $current_url = remove_query_arg('paged', $current_url); $start_page = max(1, $pagination['current_page'] - 2); $end_page = min($pagination['total_pages'], $pagination['current_page'] + 2); if ($pagination['current_page'] > 1): ?> ‹ Previous Next ›
registerPostTypes(); $easy_invoice->forceFlushRewriteRules(); // Force a complete rewrite rules flush flush_rewrite_rules(true); // One-time fix: Update all existing quotes to have proper slugs if (!get_option('easy_invoice_quotes_slugs_fixed_v3', false)) { global $wpdb; $quote_posts = $wpdb->get_results($wpdb->prepare( "SELECT ID, post_title, post_name FROM {$wpdb->posts} WHERE post_type = %s", \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE )); foreach ($quote_posts as $post) { // Generate a proper slug if empty or doesn't match our format $post_name = $post->post_name ?? ''; if (empty($post_name) || strpos($post_name, 'easy-invoice-quote') === false) { $slug = sanitize_title($post->post_title ?: 'easy-invoice-quote-' . $post->ID); wp_update_post([ 'ID' => $post->ID, 'post_name' => $slug ]); } } update_option('easy_invoice_quotes_slugs_fixed_v3', true); } // One-time fix: Publish all draft quotes to ensure proper permalinks if (!get_option('easy_invoice_quotes_published_v1', false)) { global $wpdb; $draft_quotes = $wpdb->get_results($wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_status = 'draft'", \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE )); foreach ($draft_quotes as $quote) { wp_update_post([ 'ID' => $quote->ID, 'post_status' => 'publish' ]); } update_option('easy_invoice_quotes_published_v1', true); } // Ensure all quotes have proper slugs foreach ($quotes as $quote): $quote->ensureProperSlug(); ?>
Quote Client Amount Status Issue Date Expiry Date Logs

getNumber() ?: 'QT-' . $quote->getId()); ?> getStatus(), ['draft'], true)) : ?> getId())); ?>
View Edit getStatus() === 'cancelled'): ?> Restore Delete Permanently Trash Download Export Duplicate $action_html) { echo '' . wp_kses_post($action_html) . ''; } ?>
getClientId() > 0) { $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository(); $client = $client_repository->find($quote->getClientId()); if ($client) { $client_name = $client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName()); if (empty(trim($client_name))) { $client_name = $client->getDisplayName() ?: 'Client ' . $client->getId(); } } } elseif ($quote->getCustomerName()) { $client_name = $quote->getCustomerName(); } echo esc_html($client_name); ?>
getCustomerEmail() ?: ''); ?>
format($quote->getTotal() ?: 0)); ?> getStatus() ?: 'draft'; // An open quote past its expiry date reads as expired even // before the daily expiry check has flipped its status. if (in_array($status, ['available', 'sent'], true) && $quote->getExpiryDate() && strtotime($quote->getExpiryDate()) && gmdate('Y-m-d', strtotime($quote->getExpiryDate())) < gmdate('Y-m-d', current_time('timestamp'))) { $status = 'expired'; } $status_classes = [ 'draft' => 'bg-gray-100 text-gray-800 ring-1 ring-gray-300', 'available' => 'bg-blue-100 text-blue-800 ring-1 ring-blue-300', 'sent' => 'bg-blue-100 text-blue-800 ring-1 ring-blue-300', 'accepted' => 'bg-green-100 text-green-800 ring-1 ring-green-300', 'declined' => 'bg-red-100 text-red-800 ring-1 ring-red-300', 'expired' => 'bg-yellow-100 text-yellow-800 ring-1 ring-yellow-300' ]; $status_class = $status_classes[$status] ?? 'bg-gray-100 text-gray-800 ring-1 ring-gray-300'; ?> getIssueDate() ? gmdate('M j, Y', strtotime($quote->getIssueDate())) : '—'); ?> getExpiryDate() ? gmdate('M j, Y', strtotime($quote->getExpiryDate())) : '—'); ?> getLogs($quote->getId()); $logs_count = count($all_logs); if ($logs_count > 0): ?>
1): ?>
Showing to of results
Page of
1 ? $pagination['current_page'] - 1 : 1; $next_page = $pagination['current_page'] < $pagination['total_pages'] ? $pagination['current_page'] + 1 : $pagination['total_pages']; $current_url = add_query_arg($_GET, admin_url('admin.php')); $current_url = remove_query_arg('paged', $current_url); $start_page = max(1, $pagination['current_page'] - 2); $end_page = min($pagination['total_pages'], $pagination['current_page'] + 2); if ($pagination['current_page'] > 1): ?> ‹ Previous Next ›