# easy-invoice/2.3.2/templates/admin/addons-page.php

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.3.2. 619 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.3.2/code/templates/admin/addons-page.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.3.2/raw/templates/admin/addons-page.php
- Modified: 2026-05-21T08:29:24+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/easy-invoice/2.3.2/code/templates/admin/addons-page.php#L10-L20`.

```php
<?php
/**
 * Addons admin page — matches the standard Easy Invoice page chrome
 * (p-8 outer + ei-app-page-header header bar), lives inside main-template.php.
 *
 * Expected locals (set by AddonsController::display):
 *   array  $addons        — full registry
 *   string $current_plan  — 'none' | 'professional' | 'agency'
 *   array  $upgrade_urls  — plan → checkout URL
 *
 * @package EasyInvoice
 */

use EasyInvoice\Addons\AddonManager;
use EasyInvoice\Addons\LicensePlanResolver;

if (!defined('ABSPATH')) {
    exit;
}

$addons        = $addons        ?? [];
$current_plan  = $current_plan  ?? LicensePlanResolver::PLAN_NONE;
$upgrade_urls  = $upgrade_urls  ?? [];
$current_label = LicensePlanResolver::getPlanLabel($current_plan);

// Plan pill colour classes (Tailwind utilities — same vocabulary as the rest of the app).
// Personal is the entry-level paid tier (migrated Pro features); rendered
// in teal so it's visually distinct from Professional (blue) and Agency
// (purple) — and from Free (grey).
$plan_classes = [
    LicensePlanResolver::PLAN_NONE         => ['chip' => 'bg-gray-100 text-gray-700 border-gray-200',     'bar' => 'bg-gray-300',   'icon' => 'bg-gray-100 text-gray-600 border-gray-200'],
    LicensePlanResolver::PLAN_PERSONAL     => ['chip' => 'bg-teal-50 text-teal-700 border-teal-200',       'bar' => 'bg-teal-500',   'icon' => 'bg-teal-50 text-teal-700 border-teal-200'],
    LicensePlanResolver::PLAN_PROFESSIONAL => ['chip' => 'bg-blue-50 text-blue-700 border-blue-200',       'bar' => 'bg-blue-500',   'icon' => 'bg-blue-50 text-blue-700 border-blue-200'],
    LicensePlanResolver::PLAN_AGENCY       => ['chip' => 'bg-purple-50 text-purple-700 border-purple-200', 'bar' => 'bg-purple-600', 'icon' => 'bg-purple-50 text-purple-700 border-purple-200'],
];
$hero_plan_class = $plan_classes[$current_plan] ?? $plan_classes[LicensePlanResolver::PLAN_NONE];

// Distinct categories present for the filter chips.
$categories = [];
foreach ($addons as $a) {
    $categories[$a['category']] = true;
}
$categories = array_keys($categories);
?>

<div class="p-8">

    
    <div class="ei-app-page-header bg-white border-b border-gray-200 px-6 easy-invoice-admin-page-header">
        <div class="flex items-center justify-between flex-wrap gap-4">
            <div>
                <h1 class="text-2xl font-bold text-gray-900"><?php esc_html_e('Addons', 'easy-invoice'); ?></h1>
                <p class="mt-1 text-sm text-gray-500">
                    <?php esc_html_e('Extend Easy Invoice with optional features. Enable only what you need — disabled addons add zero overhead.', 'easy-invoice'); ?>
                </p>
            </div>
            
            <div class="flex items-center gap-3">
                <div class="inline-flex items-center gap-2 text-sm text-gray-600">
                    <span class="font-medium"><?php esc_html_e('Your plan:', 'easy-invoice'); ?></span>
                    <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold border <?php echo esc_attr($hero_plan_class['chip']); ?>">
                        <?php echo esc_html($current_label); ?>
                    </span>
                </div>
                <a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-license')); ?>"
                   class="inline-flex items-center px-3 py-1.5 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
                    <?php esc_html_e('Manage license', 'easy-invoice'); ?>
                </a>
            </div>
        </div>
    </div>

    <?php if (!empty($license_banner)):
        $b = $license_banner;
        $skin = $b['type'] === 'warning'
            ? ['wrap' => 'bg-amber-50 border-amber-200',   'icon' => 'text-amber-500', 'title' => 'text-amber-900', 'body' => 'text-amber-800', 'btn' => 'bg-amber-600 hover:bg-amber-700 text-white', 'dash' => 'warning']
            : ['wrap' => 'bg-indigo-50 border-indigo-200', 'icon' => 'text-indigo-500','title' => 'text-indigo-900','body' => 'text-indigo-800','btn' => 'bg-indigo-600 hover:bg-indigo-700 text-white','dash' => 'info'];
    ?>
        <div class="mt-8 mb-2 border <?php echo esc_attr($skin['wrap']); ?> rounded-lg p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
            <div class="flex items-start gap-3">
                <div class="flex-shrink-0 <?php echo esc_attr($skin['icon']); ?>">
                    <span class="dashicons dashicons-<?php echo esc_attr($skin['dash']); ?>" style="font-size:22px;width:22px;height:22px;line-height:22px;"></span>
                </div>
                <div>
                    <p class="text-sm font-semibold <?php echo esc_attr($skin['title']); ?>"><?php echo esc_html($b['title']); ?></p>
                    <p class="mt-0.5 text-sm <?php echo esc_attr($skin['body']); ?>"><?php echo esc_html($b['message']); ?></p>
                </div>
            </div>
            <a href="<?php echo esc_url($b['cta_url']); ?>"
               <?php echo strpos($b['cta_url'], 'http') === 0 ? 'target="_blank" rel="noopener"' : ''; ?>
               class="inline-flex items-center px-4 py-2 rounded-md text-sm font-semibold shadow-sm whitespace-nowrap <?php echo esc_attr($skin['btn']); ?>">
                <?php echo esc_html($b['cta_label']); ?>
            </a>
        </div>
    <?php endif; ?>

    
    <div class="flex flex-wrap items-center gap-2 mt-6 mb-3" id="ei-addons-filters">
        <button type="button" data-filter="all" class="ei-addons-chip is-active inline-flex items-center px-4 py-1.5 rounded-full text-sm font-medium bg-gray-900 text-white">
            <?php esc_html_e('All', 'easy-invoice'); ?>
        </button>
        <?php foreach ($categories as $cat): ?>
            <button type="button" data-filter="<?php echo esc_attr($cat); ?>" class="ei-addons-chip inline-flex items-center px-4 py-1.5 rounded-full text-sm font-medium bg-white text-gray-700 border border-gray-300 hover:bg-gray-50">
                <?php echo esc_html(ucfirst($cat)); ?>
            </button>
        <?php endforeach; ?>
    </div>

    
    
    <div id="ei-addons-toolbar" class="bg-white border border-gray-200 rounded-md px-4 py-2 mb-5 flex flex-wrap items-center gap-3">

        
        <label class="inline-flex items-center gap-2 text-sm text-gray-700 cursor-pointer select-none">
            <input type="checkbox" id="ei-addons-select-all"
                   class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"/>
            <span><?php esc_html_e('Select all', 'easy-invoice'); ?></span>
            <span id="ei-addons-selected-count" class="text-xs text-gray-500"></span>
        </label>

        <button type="button" id="ei-addons-bulk-activate"
                class="inline-flex items-center gap-1 px-3 py-1.5 rounded-md text-sm font-semibold bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-50 disabled:pointer-events-none"
                disabled>
            <span class="dashicons dashicons-yes-alt" style="font-size:14px;width:14px;height:14px;"></span>
            <?php esc_html_e('Activate selected', 'easy-invoice'); ?>
        </button>
        <button type="button" id="ei-addons-bulk-deactivate"
                class="inline-flex items-center gap-1 px-3 py-1.5 rounded-md text-sm font-semibold bg-white border border-red-200 text-red-600 hover:bg-red-50 disabled:opacity-50 disabled:pointer-events-none"
                disabled>
            <span class="dashicons dashicons-dismiss" style="font-size:14px;width:14px;height:14px;"></span>
            <?php esc_html_e('Deactivate selected', 'easy-invoice'); ?>
        </button>

        
        
        <label class="inline-flex items-center gap-2 text-sm text-gray-700 ml-auto">
            <span class="dashicons dashicons-filter" style="font-size:14px;width:14px;height:14px;line-height:14px;color:#6b7280;" aria-hidden="true"></span>
            <select id="ei-addons-plan-filter"
                    class="block border border-gray-300 rounded-md py-1.5 px-3 text-sm focus:ring-indigo-500 focus:border-indigo-500">
                <option value=""><?php esc_html_e('All plans', 'easy-invoice'); ?></option>
                <option value="<?php echo esc_attr(LicensePlanResolver::PLAN_PERSONAL); ?>"><?php echo esc_html(LicensePlanResolver::getPlanLabel(LicensePlanResolver::PLAN_PERSONAL)); ?></option>
                <option value="<?php echo esc_attr(LicensePlanResolver::PLAN_PROFESSIONAL); ?>"><?php echo esc_html(LicensePlanResolver::getPlanLabel(LicensePlanResolver::PLAN_PROFESSIONAL)); ?></option>
                <option value="<?php echo esc_attr(LicensePlanResolver::PLAN_AGENCY); ?>"><?php echo esc_html(LicensePlanResolver::getPlanLabel(LicensePlanResolver::PLAN_AGENCY)); ?></option>
            </select>
        </label>

        
        <div class="relative w-56">
            <span class="dashicons dashicons-search absolute left-3 inset-y-0 my-auto h-4 text-gray-400 pointer-events-none" style="font-size:14px;width:14px;line-height:14px;" aria-hidden="true"></span>
            <input type="search" id="ei-addons-search"
                   placeholder="<?php esc_attr_e('Search addons…', 'easy-invoice'); ?>"
                   class="block w-full border border-gray-300 rounded-md py-1.5 pl-9 pr-3 text-sm focus:ring-indigo-500 focus:border-indigo-500"/>
        </div>

        <button type="button" id="ei-addons-filter-reset"
                class="text-xs font-medium text-gray-500 hover:text-indigo-600 underline-offset-2 hover:underline hidden">
            <?php esc_html_e('Clear', 'easy-invoice'); ?>
        </button>
        <span id="ei-addons-filter-count" class="text-xs text-gray-500"></span>
    </div>

    
    <div id="ei-addons-empty" class="hidden bg-white border border-dashed border-gray-300 rounded-md px-6 py-10 text-center">
        <span class="dashicons dashicons-search" style="font-size:32px;width:32px;height:32px;color:#9ca3af;"></span>
        <p class="mt-2 text-sm text-gray-600"><?php esc_html_e('No addons match your filters.', 'easy-invoice'); ?></p>
    </div>

    
    <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4" id="ei-addons-grid">
        <?php foreach ($addons as $addon):
            $status        = AddonManager::statusFor($addon['id']);
            $is_active     = $status === 'active';
            $is_locked     = $status === 'locked';
            $is_available  = $status === 'available';
            $plan_klass    = $plan_classes[$addon['plan']] ?? $plan_classes[LicensePlanResolver::PLAN_PROFESSIONAL];
            $plan_label    = LicensePlanResolver::getPlanLabel($addon['plan']);
            $plan_label_short = LicensePlanResolver::getPlanLabelShort($addon['plan']);
            $upgrade_url   = $upgrade_urls[$addon['plan']] ?? '#';
            $category      = $addon['category'];
            $badge         = $addon['badge'] ?? null;
        ?>
            <article
                class="ei-addons-card relative bg-white rounded-lg shadow border border-gray-200 hover:shadow-md transition-shadow flex flex-col"
                data-addon-id="<?php echo esc_attr($addon['id']); ?>"
                data-addon-plan="<?php echo esc_attr($addon['plan']); ?>"
                data-category="<?php echo esc_attr($category); ?>"
                data-search-text="<?php echo esc_attr(strtolower(trim(($addon['name'] ?? '') . ' ' . ($addon['short_name'] ?? '') . ' ' . ($addon['tagline'] ?? '') . ' ' . ($addon['description'] ?? '')))); ?>"
            >
                

                <div class="p-4 flex-1 flex flex-col">
                    
                    <div class="flex items-start justify-between mb-3">
                        <div class="flex items-center gap-2">
                            <?php if (!$is_locked): ?>
                                <input type="checkbox"
                                       class="ei-addons-row-check h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
                                       data-addon-id="<?php echo esc_attr($addon['id']); ?>"
                                       data-addon-status="<?php echo esc_attr($status); ?>"
                                       aria-label="<?php echo esc_attr(sprintf(__('Select %s for bulk action', 'easy-invoice'), $addon['name'])); ?>"/>
                            <?php endif; ?>
                            <div class="rounded-full border <?php echo esc_attr($plan_klass['icon']); ?> inline-flex items-center justify-center"
                                 style="width:24px;height:24px;">
                                <span class="dashicons <?php echo esc_attr($addon['icon']); ?>"
                                      style="font-size:12px;width:12px;height:12px;line-height:12px;"></span>
                            </div>
                        </div>
                        <div class="flex items-center flex-wrap gap-1 justify-end">
                            <span class="inline-flex items-center leading-none rounded-full font-semibold uppercase border <?php echo esc_attr($plan_klass['chip']); ?>"
                                  style="font-size:9px; letter-spacing:0.03em; padding:3px 7px;">
                                <?php echo esc_html($plan_label); ?>
                            </span>
                            <?php
                            // "New" badge removed per design — the plan
                            // pill alone is the addon's identity tag. The
                            // $addon['badge'] field is still respected for
                            // future variants (e.g. 'popular'), just not
                            // for the 'new' state.
                            ?>
                            <?php if ($is_locked): ?>
                                <span class="text-gray-400" title="<?php esc_attr_e('Locked — upgrade required', 'easy-invoice'); ?>">
                                    <span class="dashicons dashicons-lock" style="font-size:11px;width:11px;height:11px;line-height:11px;"></span>
                                </span>
                            <?php endif; ?>
                        </div>
                    </div>

                    
                    <h3 class="text-sm font-semibold text-gray-900 leading-snug mb-2"><?php echo esc_html($addon['name']); ?></h3>
                    <?php if (!empty($addon['description'])):
                        // Show the full text in a hover popover only when the
                        // visible clamp (3 lines × ~60 chars) would actually
                        // hide content; otherwise the truncated paragraph IS
                        // the full text and a popover would just be noise.
                        $needs_popover = mb_strlen($addon['description']) > 140;
                    ?>
                        <div class="ei-addon-desc-wrap<?php echo $needs_popover ? ' has-popover' : ''; ?>">
                            <p class="ei-addon-desc text-xs text-gray-500 leading-relaxed flex-1 mb-3">
                                <?php echo esc_html($addon['description']); ?>
                            </p>
                            <?php if ($needs_popover): ?>
                                <div class="ei-addon-desc-popover" role="tooltip" aria-hidden="true">
                                    <?php echo esc_html($addon['description']); ?>
                                </div>
                            <?php endif; ?>
                        </div>
                    <?php endif; ?>

                    
                    <div class="pt-3 border-t border-gray-100 flex items-center justify-between gap-2">
                        <?php if ($is_active): ?>
                            
                            <div class="flex items-center gap-2 min-w-0 flex-wrap">
                                <span class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs font-semibold bg-green-100 text-green-800">
                                    <span class="dashicons dashicons-yes" style="font-size:12px;width:12px;height:12px;line-height:12px;"></span>
                                    <?php esc_html_e('Active', 'easy-invoice'); ?>
                                </span>
                                <button
                                    type="button"
                                    class="ei-addons-btn ei-addons-btn--toggle text-xs font-medium text-red-600 hover:text-red-800 underline-offset-2 hover:underline"
                                    data-action="disable"
                                >
                                    <?php esc_html_e('Deactivate', 'easy-invoice'); ?>
                                </button>
                            </div>
                            <div class="flex items-center gap-3 whitespace-nowrap">
                                <?php if (!empty($addon['docs_url'])): ?>
                                    <a href="<?php echo esc_url($addon['docs_url']); ?>"
                                       target="_blank"
                                       rel="noopener"
                                       class="text-xs font-medium text-gray-500 hover:text-indigo-600"
                                       title="<?php echo esc_attr(sprintf(__('Open %s documentation', 'easy-invoice'), $addon['name'])); ?>">
                                        <?php esc_html_e('Docs', 'easy-invoice'); ?> →
                                    </a>
                                <?php endif; ?>
                                <?php if (!empty($addon['settings_url'])): ?>
                                    <a href="<?php echo esc_url($addon['settings_url']); ?>"
                                       class="text-xs font-medium text-indigo-600 hover:text-indigo-800 underline-offset-2 hover:underline"
                                       title="<?php echo esc_attr(sprintf(__('Open %s settings', 'easy-invoice'), $addon['name'])); ?>">
                                        <?php esc_html_e('Settings', 'easy-invoice'); ?> →
                                    </a>
                                <?php endif; ?>
                            </div>
                        <?php elseif ($is_locked): ?>
                            <button
                                type="button"
                                class="ei-addons-btn ei-addons-btn--upgrade inline-flex items-center px-2 py-1 rounded-md text-xs font-semibold text-white shadow-sm <?php echo $addon['plan'] === LicensePlanResolver::PLAN_AGENCY ? 'bg-purple-600 hover:bg-purple-700' : 'bg-blue-600 hover:bg-blue-700'; ?> transition-colors"
                                data-action="upgrade"
                                data-upgrade-url="<?php echo esc_url($upgrade_url); ?>"
                                data-required-plan="<?php echo esc_attr($addon['plan']); ?>"
                                data-required-plan-label="<?php echo esc_attr($plan_label); ?>"
                                data-feature-name="<?php echo esc_attr($addon['name']); ?>"
                                data-feature-description="<?php echo esc_attr($addon['description']); ?>"
                            >
                                <?php echo esc_html(sprintf(__('Upgrade to %s', 'easy-invoice'), $plan_label)); ?>
                            </button>
                            <a href="<?php echo esc_url($addon['docs_url']); ?>" target="_blank" rel="noopener" class="text-xs font-medium text-gray-500 hover:text-indigo-600 whitespace-nowrap">
                                <?php esc_html_e('Learn', 'easy-invoice'); ?> →
                            </a>
                        <?php else: // available ?>
                            <button
                                type="button"
                                class="ei-addons-btn ei-addons-btn--toggle inline-flex items-center px-3 py-1 rounded-md text-xs font-semibold bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm transition-colors"
                                data-action="enable"
                            >
                                <?php esc_html_e('Activate', 'easy-invoice'); ?>
                            </button>
                            <a href="<?php echo esc_url($addon['docs_url']); ?>" target="_blank" rel="noopener" class="text-xs font-medium text-gray-500 hover:text-indigo-600 whitespace-nowrap">
                                <?php esc_html_e('Learn more', 'easy-invoice'); ?> →
                            </a>
                        <?php endif; ?>
                    </div>
                </div>
            </article>
        <?php endforeach; ?>
    </div>

    <?php if (empty($addons)): ?>
        <div class="bg-white border border-dashed border-gray-300 rounded-lg p-12 text-center text-gray-500 mt-6">
            <p><?php esc_html_e('No addons are registered yet.', 'easy-invoice'); ?></p>
        </div>
    <?php endif; ?>

</div>

<style>
.ei-addons-card.is-hidden { display: none !important; }
.ei-addons-btn.is-loading { opacity: 0.6; pointer-events: none; }

/* Lift the hovered card above its siblings so the description popover
   (which uses position: absolute) isn't visually clipped by neighbouring
   cards' shadows or stacking context. */
.ei-addons-card { z-index: 1; }
.ei-addons-card:hover,
.ei-addons-card:focus-within { z-index: 20; }

/* Addon description: 3-line clamp with a hover popover for the full text
   when the description is long enough to be truncated. Replaces the native
   browser `title` tooltip with a styled in-card popover. */
.ei-addon-desc-wrap { position: relative; flex: 1; margin-bottom: 0.75rem; }
.ei-addon-desc {
    margin: 0;
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.ei-addon-desc-wrap.has-popover .ei-addon-desc { cursor: help; }
.ei-addon-desc-popover {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    right: 0;
    z-index: 30;
    background: #1f2937;
    color: #f9fafb;
    padding: 10px 12px;
    border-radius: 6px;
    font-size: 11px;
    line-height: 1.55;
    box-shadow: 0 12px 28px rgba(0, 0, 0, 0.18), 0 4px 10px rgba(0, 0, 0, 0.08);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-4px);
    transition: opacity 140ms ease, transform 140ms ease, visibility 140ms;
    pointer-events: none;
}
.ei-addon-desc-popover::before {
    content: '';
    position: absolute;
    top: -5px;
    left: 16px;
    width: 10px;
    height: 10px;
    background: inherit;
    transform: rotate(45deg);
    box-shadow: -2px -2px 4px rgba(0, 0, 0, 0.04);
}
.ei-addon-desc-wrap.has-popover:hover .ei-addon-desc-popover,
.ei-addon-desc-wrap.has-popover:focus-within .ei-addon-desc-popover {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}
/* When a card is in the last column / right edge, flip the popover to
   the right so it doesn't overflow off-screen on narrow viewports. */
@media (max-width: 640px) {
    .ei-addon-desc-popover { font-size: 12px; padding: 12px 14px; }
}
</style>

<script>
// Bulk activate / deactivate — fans out the existing single-toggle AJAX
// endpoint in parallel. Each row reports its own success/failure toast via
// the per-card click path the single-toggle code already wires up, so the
// only thing this block manages is selection + dispatch.
(function () {
    'use strict';

    var bulkActivate   = document.getElementById('ei-addons-bulk-activate');
    var bulkDeactivate = document.getElementById('ei-addons-bulk-deactivate');
    var selectAll      = document.getElementById('ei-addons-select-all');
    var counter        = document.getElementById('ei-addons-selected-count');
    if (!bulkActivate || !bulkDeactivate || !selectAll) return;

    function rowChecks() {
        return Array.prototype.slice.call(document.querySelectorAll('.ei-addons-row-check'));
    }
    function selectedIds(filterStatus) {
        return rowChecks()
            .filter(function (cb) { return cb.checked; })
            .filter(function (cb) { return !filterStatus || cb.dataset.addonStatus === filterStatus; })
            .map(function (cb) { return cb.dataset.addonId; });
    }
    function refreshButtons() {
        var checked = rowChecks().filter(function (cb) { return cb.checked; });
        var n = checked.length;
        counter.textContent = n > 0 ? '(' + n + ')' : '';
        // Activate is enabled only when at least one selected row is `available`.
        // Deactivate is enabled only when at least one selected row is `active`.
        bulkActivate.disabled   = !checked.some(function (cb) { return cb.dataset.addonStatus === 'available'; });
        bulkDeactivate.disabled = !checked.some(function (cb) { return cb.dataset.addonStatus === 'active'; });
    }

    selectAll.addEventListener('change', function () {
        rowChecks().forEach(function (cb) {
            // Don't override a row that's locked (not selectable) — locked rows
            // don't render a checkbox, so this is just per-visible-card.
            cb.checked = selectAll.checked;
        });
        refreshButtons();
    });
    document.addEventListener('change', function (e) {
        if (e.target && e.target.classList && e.target.classList.contains('ei-addons-row-check')) {
            refreshButtons();
        }
    });

    /**
     * Direct AJAX bulk dispatcher.
     *
     * The previous implementation tried to "click each card's single-toggle
     * button" and watch jQuery's $.active counter. That had two real bugs:
     *
     *   1. confirm-helper.js runs in CAPTURE phase and *removes*
     *      dataset.eiConfirmed before any bubble-phase handler can read it,
     *      so the Deactivate-selected click path always evaluated to "not
     *      confirmed" and silently no-op'd.
     *
     *   2. Even when activate worked, the FIRST AJAX to respond would call
     *      window.location.reload() (the single-toggle success path) and
     *      cancel every sibling in-flight request. Net result: only one
     *      addon would flip at a time.
     *
     * The fix: bypass the single-toggle path entirely. Fire $.post directly
     * here, $.when() all promises, reload exactly once when all settle.
     */
    function dispatchBulk(action) {
        var status = action === 'enable' ? 'available' : 'active';
        var ids    = selectedIds(status);
        if (!ids.length) return;
        if (!window.jQuery) {
            alert('Cannot bulk-toggle: jQuery missing on this page.');
            return;
        }
        var $   = window.jQuery;
        var cfg = window.EasyInvoiceAddons || {};
        if (!cfg.ajaxUrl || !cfg.nonce) {
            alert('Cannot bulk-toggle: addons-page config missing.');
            return;
        }

        // Visual feedback — both buttons go disabled with "Working…" label
        // so the user knows something is happening.
        var origActivateText   = bulkActivate.innerHTML;
        var origDeactivateText = bulkDeactivate.innerHTML;
        bulkActivate.disabled   = true;
        bulkDeactivate.disabled = true;
        var activeBtn = action === 'enable' ? bulkActivate : bulkDeactivate;
        activeBtn.innerHTML = action === 'enable'
            ? '<?php echo esc_js(__('Activating…', 'easy-invoice')); ?>'
            : '<?php echo esc_js(__('Deactivating…', 'easy-invoice')); ?>';

        // Mark in flight so any other lingering single-toggle handler skips
        // its own reload (belt-and-braces with the dedicated $.when below).
        window.EasyInvoiceAddons = window.EasyInvoiceAddons || {};
        window.EasyInvoiceAddons.bulkInFlight = true;

        var requests = ids.map(function (id) {
            return $.post(cfg.ajaxUrl, {
                action:   'easy_invoice_toggle_addon',
                _nonce:   cfg.nonce,
                addon_id: id,
                enabled:  action === 'enable' ? 1 : 0
            });
        });

        // $.when treats EACH deferred individually — we don't care about
        // per-row results because the page reload will repaint everything
        // from server state. We only need to know when ALL settle (success
        // or failure) so we can reload exactly once.
        var settled = 0;
        var failed  = 0;
        function onSettled(ok) {
            settled++;
            if (!ok) failed++;
            if (settled === requests.length) {
                window.EasyInvoiceAddons.bulkInFlight = false;
                if (window.EasyInvoiceToast && window.EasyInvoiceToast[failed ? 'warning' : 'success']) {
                    window.EasyInvoiceToast[failed ? 'warning' : 'success'](
                        failed
                            ? (failed + ' of ' + requests.length + ' addon(s) failed. The page will refresh.')
                            : (requests.length + ' addon(s) updated.')
                    );
                }
                // Small delay so the toast is visible before the reload kills it.
                setTimeout(function () { window.location.reload(); }, 600);
            }
        }
        requests.forEach(function (req) {
            req.done(function (resp) { onSettled(resp && resp.success); })
               .fail(function () { onSettled(false); });
        });

        // Clear selection UI immediately so the user sees the click landed.
        selectAll.checked = false;
        rowChecks().forEach(function (cb) { cb.checked = false; });
        // Keep the bulk buttons disabled while in flight — refreshButtons
        // would re-enable them but we want them locked until reload.
    }

    bulkActivate.addEventListener('click', function () {
        dispatchBulk('enable');
    });

    // Bulk deactivate: drive the EasyInvoiceConfirmation modal directly
    // instead of going through data-ei-confirm + confirm-helper (which
    // strips its dataset marker too early — see dispatchBulk comments).
    bulkDeactivate.addEventListener('click', function () {
        var doIt = function () { dispatchBulk('disable'); };
        if (window.EasyInvoiceConfirmation && typeof window.EasyInvoiceConfirmation.show === 'function') {
            window.EasyInvoiceConfirmation.show({
                title:        '<?php echo esc_js(__('Deactivate selected addons', 'easy-invoice')); ?>',
                message:      '<?php echo esc_js(__('Deactivate every selected addon? Their settings stay saved.', 'easy-invoice')); ?>',
                confirmText:  '<?php echo esc_js(__('Deactivate', 'easy-invoice')); ?>',
                confirmClass: 'bg-red-600 hover:bg-red-700',
                onConfirm:    doIt
            });
        } else if (window.confirm('<?php echo esc_js(__('Deactivate every selected addon?', 'easy-invoice')); ?>')) {
            doIt();
        }
    });

    refreshButtons();
})();

// ── Plan-filter + search ─────────────────────────────────────────
(function () {
    var planSelect = document.getElementById('ei-addons-plan-filter');
    var searchInp  = document.getElementById('ei-addons-search');
    var resetBtn   = document.getElementById('ei-addons-filter-reset');
    var countSpan  = document.getElementById('ei-addons-filter-count');
    var emptyState = document.getElementById('ei-addons-empty');
    var grid       = document.getElementById('ei-addons-grid');
    if (!planSelect || !searchInp || !grid) return;

    var cards = Array.prototype.slice.call(grid.querySelectorAll('.ei-addons-card'));

    function applyFilters() {
        var plan = planSelect.value || '';
        var q    = (searchInp.value || '').trim().toLowerCase();
        var shown = 0;
        cards.forEach(function (card) {
            var cardPlan = card.getAttribute('data-addon-plan') || '';
            var cardText = card.getAttribute('data-search-text') || '';
            var planOk   = !plan || cardPlan === plan;
            var qOk      = !q    || cardText.indexOf(q) !== -1;
            var visible  = planOk && qOk;
            card.classList.toggle('hidden', !visible);
            if (visible) shown++;
        });

        // Empty-state + count.
        if (emptyState) emptyState.classList.toggle('hidden', shown > 0);
        if (countSpan) {
            countSpan.textContent = (plan || q)
                ? (shown + ' / ' + cards.length + ' shown')
                : '';
        }

        // Reveal "Clear filters" only when at least one filter is active.
        if (resetBtn) {
            resetBtn.classList.toggle('hidden', !plan && !q);
        }
    }

    planSelect.addEventListener('change', applyFilters);
    searchInp.addEventListener('input', applyFilters);
    if (resetBtn) {
        resetBtn.addEventListener('click', function () {
            planSelect.value = '';
            searchInp.value  = '';
            applyFilters();
            searchInp.focus();
        });
    }
})();
</script>
<?php
// Wire up the data-ei-confirm modal so the bulk-deactivate button gets the
// project's custom confirmation dialog instead of a native window.confirm.
$ei_confirm_helper = defined('EASY_INVOICE_PLUGIN_PATH')
    ? EASY_INVOICE_PLUGIN_PATH . 'templates/admin/partials/confirm-helper.php'
    : EASY_INVOICE_PLUGIN_DIR . 'templates/admin/partials/confirm-helper.php';
if ($ei_confirm_helper && file_exists($ei_confirm_helper)) {
    include $ei_confirm_helper;
}
?>

```
