Files
kristianwindandClaude Opus 5 bf4b429014 Put a Discounts card on the shared Nolimit dashboard
The dashboard showed Cache, Security and Assistant and not this plugin. Being
absent from the dashboard is a different failure from being absent from the
menu, and Kristian hit them in that order — so each now has its own assertion
rather than one standing in for the other.

The card registers on wpmgr_dashboard_cards with the counts this plugin exists
to report: groups, and addresses currently receiving a discount.

Cached for five minutes. The contract says implementations must not measure
anything in that filter, because it runs on an admin page load and a slow card
makes somebody else's dashboard slow in a way they cannot attribute to us. Two
COUNT(*) on small indexed tables is the most this may cost, and the dashboard
is a page people leave open.

Status is warn rather than fail when nothing is configured: a shop that has
installed this and not set it up yet is unfinished, not broken. Reserving red
for actual breakage is what keeps red worth reading.

Two assertions, proven red by removing the add_filter:

  FAIL  this plugin puts a card on the shared dashboard — 0 card(s) registered
  FAIL  and it carries this plugin colour, not WooCommerce purple
        — expected '#1a7a52', got NULL

The colour is matched against the literal, not against a constant. The shared
file renders whatever colour it is handed, so a card asserting its own constant
would agree with itself after somebody put WooCommerce purple back — the same
self-agreeing shape as the REST namespace assertion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 07:57:02 +02:00

399 lines
12 KiB
PHP

<?php
/**
* Admin menu, tabs and routing.
*
* @package WC_Email_Discount_Manager
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Registers the admin page under WooCommerce and routes tabs/actions.
*/
class WC_EDM_Admin {
/**
* Admin page slug.
*/
/** Cache key for the dashboard card's counts. */
const CARD_TRANSIENT = 'wc_edm_card_counts';
const PAGE_SLUG = 'wc-email-discounts';
/**
* Groups controller.
*
* @var WC_EDM_Admin_Groups
*/
private $groups;
/**
* Emails controller.
*
* @var WC_EDM_Admin_Emails
*/
private $emails;
/**
* Shipping controller.
*
* @var WC_EDM_Admin_Shipping
*/
private $shipping;
/**
* Audit log controller.
*
* @var WC_EDM_Admin_Audit
*/
private $audit;
/**
* Hook everything up.
*/
public function __construct() {
// Each sub-controller lives in its own file. Guarding here means an
// incomplete plugin folder costs a tab, not the whole admin screen.
$this->groups = class_exists( 'WC_EDM_Admin_Groups' ) ? new WC_EDM_Admin_Groups() : null;
$this->emails = class_exists( 'WC_EDM_Admin_Emails' ) ? new WC_EDM_Admin_Emails() : null;
$this->shipping = class_exists( 'WC_EDM_Admin_Shipping' ) ? new WC_EDM_Admin_Shipping() : null;
$this->audit = class_exists( 'WC_EDM_Admin_Audit' ) ? new WC_EDM_Admin_Audit() : null;
add_action( 'admin_menu', array( $this, 'register_menu' ) );
add_filter( 'wpmgr_dashboard_cards', array( $this, 'dashboard_card' ) );
add_action( 'admin_init', array( $this, 'handle_actions' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
}
/**
* This plugin's card on the shared Nolimit dashboard.
*
* The contract says implementations must not measure anything here,
* because this filter runs on an admin page load and a slow card makes
* somebody else's dashboard slow in a way they cannot attribute. Two
* COUNT(*) on small indexed tables is the most this may cost, and even
* that is cached: the dashboard is a page people leave open.
*
* @param array $cards The cards so far.
* @return array
*/
public function dashboard_card( $cards ) {
$counts = get_transient( self::CARD_TRANSIENT );
if ( ! is_array( $counts ) ) {
$counts = array(
'groups' => WC_EDM_DB::count_groups(),
'emails' => WC_EDM_DB::count_emails( true ),
);
set_transient( self::CARD_TRANSIENT, $counts, 5 * MINUTE_IN_SECONDS );
}
// "warn", not "fail": a shop that has installed this and not set it up
// yet is not broken, it is unfinished. Reserving red for actual
// breakage is what keeps red worth reading.
$status = $counts['groups'] > 0 && $counts['emails'] > 0 ? 'ok' : 'warn';
$cards[] = array(
'title' => __( 'Discounts', 'wc-email-discount' ),
'status' => $status,
'headline' => sprintf(
/* translators: %s: number of email addresses that currently receive a discount. */
_n( '%s address gets a discount', '%s addresses get a discount', $counts['emails'], 'wc-email-discount' ),
number_format_i18n( $counts['emails'] )
),
'lines' => array(
__( 'Groups', 'wc-email-discount' ) => number_format_i18n( $counts['groups'] ),
__( 'Active addresses', 'wc-email-discount' ) => number_format_i18n( $counts['emails'] ),
),
'url' => admin_url( 'admin.php?page=' . self::PAGE_SLUG ),
// The plugin's own colour. Deliberately not WooCommerce's brand
// purple; see the note in assets/icon.svg.
'colour' => '#1a7a52',
);
return $cards;
}
/**
* Register the page under the shared Nolimit menu.
*
* wpmgr_ensure_menu() must be called first and is a no-op when another of
* the five plugins already registered the parent — whichever booted first
* owns it, and none of them may assume it is theirs.
*
* The page slug is unchanged, so admin.php?page=… still resolves: moving a
* page between parents does not move its URL, and an existing bookmark or
* a link in an email keeps working.
*/
public function register_menu() {
if ( function_exists( 'wpmgr_ensure_menu' ) ) {
wpmgr_ensure_menu();
$parent = WPMGR_MENU_SLUG;
} else {
// The shared file is missing — a half-copied upgrade. Fall back to
// the old home rather than losing the page entirely.
$parent = 'woocommerce';
}
add_submenu_page(
$parent,
__( 'Discounts', 'wc-email-discount' ),
__( 'Discounts', 'wc-email-discount' ),
'manage_woocommerce',
self::PAGE_SLUG,
array( $this, 'render_page' )
);
}
/**
* Enqueue admin CSS/JS on the plugin page only.
*
* @param string $hook Current admin page hook.
*/
public function enqueue_assets( $hook ) {
if ( false === strpos( $hook, self::PAGE_SLUG ) ) {
return;
}
wp_enqueue_style(
'wc-edm-admin',
WC_EDM_PLUGIN_URL . 'assets/css/admin.css',
array(),
WC_EDM_VERSION
);
wp_enqueue_script(
'wc-edm-admin',
WC_EDM_PLUGIN_URL . 'assets/js/admin.js',
array( 'jquery', 'jquery-ui-sortable' ),
WC_EDM_VERSION,
true
);
wp_localize_script(
'wc-edm-admin',
'wcEdmAdmin',
array(
'confirmDelete' => __( 'Are you sure you want to delete this item? This cannot be undone.', 'wc-email-discount' ),
)
);
}
/**
* Get the currently active tab.
*
* @return string
*/
public function current_tab() {
$tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'dashboard'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$tabs = array_keys( $this->get_tabs() );
return in_array( $tab, $tabs, true ) ? $tab : 'dashboard';
}
/**
* Tab definitions.
*
* @return array
*/
private function get_tabs() {
$tabs = array( 'dashboard' => __( 'Dashboard', 'wc-email-discount' ) );
// Only offer tabs whose controller actually loaded.
if ( $this->groups ) {
$tabs['groups'] = __( 'Discount Groups', 'wc-email-discount' );
}
if ( $this->emails ) {
$tabs['emails'] = __( 'Email Assignments', 'wc-email-discount' );
}
if ( $this->shipping ) {
$tabs['shipping'] = __( 'Shipping Rules', 'wc-email-discount' );
}
if ( $this->audit ) {
$tabs['audit'] = __( 'Audit Log', 'wc-email-discount' );
}
return $tabs;
}
/**
* Handle POST/GET actions before any output (allows redirects).
*/
public function handle_actions() {
if ( ! isset( $_GET['page'] ) || self::PAGE_SLUG !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return;
}
switch ( $this->current_tab() ) {
case 'groups':
$this->groups->handle_actions();
break;
case 'emails':
$this->emails->handle_actions();
break;
case 'shipping':
$this->shipping->handle_actions();
break;
case 'audit':
$this->audit->handle_actions();
break;
}
}
/**
* Render the admin page with tab navigation.
*/
public function render_page() {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( esc_html__( 'You do not have permission to access this page.', 'wc-email-discount' ) );
}
$current = $this->current_tab();
?>
<div class="wrap wc-edm-wrap">
<h1><?php esc_html_e( 'Email Discounts', 'wc-email-discount' ); ?></h1>
<?php $this->render_notices(); ?>
<nav class="nav-tab-wrapper wc-edm-nav">
<?php foreach ( $this->get_tabs() as $slug => $label ) : ?>
<a href="<?php echo esc_url( WC_EDM_Helpers::admin_url( array( 'tab' => $slug ) ) ); ?>"
class="nav-tab <?php echo $current === $slug ? 'nav-tab-active' : ''; ?>">
<?php echo esc_html( $label ); ?>
</a>
<?php endforeach; ?>
</nav>
<div class="wc-edm-content">
<?php
switch ( $current ) {
case 'groups':
$this->groups->render();
break;
case 'emails':
$this->emails->render();
break;
case 'shipping':
$this->shipping->render();
break;
case 'audit':
$this->audit->render();
break;
default:
$this->render_dashboard();
break;
}
?>
</div>
</div>
<?php
}
/**
* Render success/error notices based on the ?message= query arg.
*/
private function render_notices() {
if ( ! isset( $_GET['message'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
$message = sanitize_key( wp_unslash( $_GET['message'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$messages = array(
'saved' => array( 'success', __( 'Saved successfully.', 'wc-email-discount' ) ),
'deleted' => array( 'success', __( 'Deleted successfully.', 'wc-email-discount' ) ),
'error' => array( 'error', __( 'Something went wrong. Please try again.', 'wc-email-discount' ) ),
);
if ( ! isset( $messages[ $message ] ) ) {
return;
}
list( $type, $text ) = $messages[ $message ];
printf(
'<div class="notice notice-%1$s is-dismissible"><p>%2$s</p></div>',
esc_attr( $type ),
esc_html( $text )
);
}
/**
* Render the dashboard tab: stats cards, quick actions, recent groups.
*/
private function render_dashboard() {
$group_count = WC_EDM_DB::count_groups();
$email_count = WC_EDM_DB::count_emails();
$active_count = WC_EDM_DB::count_emails( true );
$recent = WC_EDM_DB::get_recent_groups( 5 );
?>
<div class="wc-edm-stats">
<div class="wc-edm-stat-card">
<span class="wc-edm-stat-number"><?php echo esc_html( $group_count ); ?></span>
<span class="wc-edm-stat-label"><?php esc_html_e( 'Discount Groups', 'wc-email-discount' ); ?></span>
</div>
<div class="wc-edm-stat-card">
<span class="wc-edm-stat-number"><?php echo esc_html( $email_count ); ?></span>
<span class="wc-edm-stat-label"><?php esc_html_e( 'Email Assignments', 'wc-email-discount' ); ?></span>
</div>
<div class="wc-edm-stat-card">
<span class="wc-edm-stat-number"><?php echo esc_html( $active_count ); ?></span>
<span class="wc-edm-stat-label"><?php esc_html_e( 'Active Assignments', 'wc-email-discount' ); ?></span>
</div>
</div>
<h2><?php esc_html_e( 'Quick Actions', 'wc-email-discount' ); ?></h2>
<p class="wc-edm-quick-actions">
<a class="button button-primary" href="<?php echo esc_url( WC_EDM_Helpers::admin_url( array( 'tab' => 'groups', 'action' => 'new' ) ) ); ?>">
<?php esc_html_e( 'Add Discount Group', 'wc-email-discount' ); ?>
</a>
<a class="button button-primary" href="<?php echo esc_url( WC_EDM_Helpers::admin_url( array( 'tab' => 'emails', 'action' => 'new' ) ) ); ?>">
<?php esc_html_e( 'Add Email Assignment', 'wc-email-discount' ); ?>
</a>
<a class="button" href="<?php echo esc_url( WC_EDM_Helpers::admin_url( array( 'tab' => 'shipping' ) ) ); ?>">
<?php esc_html_e( 'Manage Shipping Rules', 'wc-email-discount' ); ?>
</a>
</p>
<h2><?php esc_html_e( 'Recent Discount Groups', 'wc-email-discount' ); ?></h2>
<?php if ( empty( $recent ) ) : ?>
<p><?php esc_html_e( 'No discount groups yet. Create your first one to get started.', 'wc-email-discount' ); ?></p>
<?php else : ?>
<table class="widefat striped wc-edm-table">
<thead>
<tr>
<th><?php esc_html_e( 'Name', 'wc-email-discount' ); ?></th>
<th><?php esc_html_e( 'Discount', 'wc-email-discount' ); ?></th>
<th><?php esc_html_e( 'Categories', 'wc-email-discount' ); ?></th>
<th><?php esc_html_e( 'Created', 'wc-email-discount' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $recent as $group ) : ?>
<tr>
<td>
<a href="<?php echo esc_url( WC_EDM_Helpers::admin_url( array( 'tab' => 'groups', 'action' => 'edit', 'id' => (int) $group->id ) ) ); ?>">
<strong><?php echo esc_html( $group->name ); ?></strong>
</a>
</td>
<td><?php echo esc_html( wc_format_localized_decimal( $group->discount_percent ) ); ?>%</td>
<td><?php echo esc_html( count( WC_EDM_DB::get_group_category_ids( (int) $group->id ) ) ); ?></td>
<td><?php echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $group->created_at ) ) ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php
}
}