Files
WooCleanup/includes/class-admin.php
Malin 0f26541113 feat: initial release of WooCleanup inactive account manager
- Daily WP-Cron scans all customer-role accounts
- Flags users with no completed/processing order in configurable period (default 18 months)
- Sends configurable HTML notice email with placeholder support
- Deletes account after configurable grace period (default 7 days)
- Admin UI under WooCommerce menu: flagged account list with tabs,
  manual run/send/delete actions, countdown to deletion
- Settings page: inactivity period, grace period, email subject/body
- HPOS compatible; never touches admin accounts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 17:47:49 +01:00

593 lines
28 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* WooCleanup Admin UI
*
* Adds two pages under the WooCommerce menu:
* 1. Account Cleanup table of flagged accounts with action buttons.
* 2. Cleanup Settings configurable options.
*/
defined( 'ABSPATH' ) || exit;
class WooCleanup_Admin {
/** @var WooCleanup */
private WooCleanup $plugin;
public function __construct( WooCleanup $plugin ) {
$this->plugin = $plugin;
add_action( 'admin_menu', [ $this, 'add_menu' ] );
add_action( 'admin_post_woo_cleanup_save_settings', [ $this, 'save_settings' ] );
add_action( 'admin_post_woo_cleanup_manual_run', [ $this, 'handle_manual_run' ] );
add_action( 'admin_post_woo_cleanup_send_notice', [ $this, 'handle_send_notice' ] );
add_action( 'admin_post_woo_cleanup_delete_user', [ $this, 'handle_delete_user' ] );
add_action( 'admin_notices', [ $this, 'show_admin_notices' ] );
}
// =========================================================================
// Menu Registration
// =========================================================================
public function add_menu(): void {
add_submenu_page(
'woocommerce',
__( 'Account Cleanup', 'woo-cleanup' ),
__( 'Account Cleanup', 'woo-cleanup' ),
'manage_woocommerce',
'woo-cleanup',
[ $this, 'render_accounts_page' ]
);
add_submenu_page(
'woocommerce',
__( 'Cleanup Settings', 'woo-cleanup' ),
__( 'Cleanup Settings', 'woo-cleanup' ),
'manage_woocommerce',
'woo-cleanup-settings',
[ $this, 'render_settings_page' ]
);
}
// =========================================================================
// Accounts List Page
// =========================================================================
public function render_accounts_page(): void {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( esc_html__( 'You do not have permission to view this page.', 'woo-cleanup' ) );
}
$options = $this->plugin->get_options();
$grace_days = absint( $options['grace_days'] );
$active_tab = isset( $_GET['tab'] ) && $_GET['tab'] === 'notice_sent'
? 'notice_sent'
: 'pending_notice';
$pending_users = WooCleanup::get_flagged_users( 'pending_notice' );
$noticed_users = WooCleanup::get_flagged_users( 'notice_sent' );
$last_run = get_option( 'woo_cleanup_last_run' );
// Cron next run
$next_run = wp_next_scheduled( WooCleanup::CRON_HOOK );
$users_to_show = $active_tab === 'notice_sent' ? $noticed_users : $pending_users;
?>
<div class="wrap">
<h1><?php esc_html_e( 'WooCleanup Inactive Account Manager', 'woo-cleanup' ); ?></h1>
<?php $this->output_admin_styles(); ?>
<!-- Summary bar -->
<div class="woo-cleanup-summary">
<div class="woo-cleanup-stat">
<span class="count"><?php echo count( $pending_users ); ?></span>
<span class="label"><?php esc_html_e( 'Awaiting Notice', 'woo-cleanup' ); ?></span>
</div>
<div class="woo-cleanup-stat woo-cleanup-stat--warning">
<span class="count"><?php echo count( $noticed_users ); ?></span>
<span class="label"><?php esc_html_e( 'In Grace Period', 'woo-cleanup' ); ?></span>
</div>
<div class="woo-cleanup-meta">
<?php if ( $last_run ) : ?>
<span><?php
printf(
/* translators: %s: human-readable time difference */
esc_html__( 'Last run: %s ago', 'woo-cleanup' ),
esc_html( human_time_diff( $last_run ) )
);
?></span>
<?php else : ?>
<span><?php esc_html_e( 'Cron has not run yet.', 'woo-cleanup' ); ?></span>
<?php endif; ?>
<?php if ( $next_run ) : ?>
&nbsp;|&nbsp;<span><?php
printf(
esc_html__( 'Next run: in %s', 'woo-cleanup' ),
esc_html( human_time_diff( $next_run ) )
);
?></span>
<?php endif; ?>
</div>
<!-- Manual run button -->
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"
style="margin-left:auto">
<?php wp_nonce_field( 'woo_cleanup_manual_run', '_wpnonce' ); ?>
<input type="hidden" name="action" value="woo_cleanup_manual_run">
<button type="submit" class="button button-secondary">
&#9654; <?php esc_html_e( 'Run Cleanup Now', 'woo-cleanup' ); ?>
</button>
</form>
</div>
<?php if ( empty( $options['enabled'] ) ) : ?>
<div class="notice notice-warning inline">
<p>
<?php esc_html_e( 'WooCleanup is currently disabled. Enable it on the ', 'woo-cleanup' ); ?>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=woo-cleanup-settings' ) ); ?>">
<?php esc_html_e( 'Settings page', 'woo-cleanup' ); ?>
</a>.
</p>
</div>
<?php endif; ?>
<!-- Tabs -->
<h2 class="nav-tab-wrapper" style="margin-top:16px">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=woo-cleanup&tab=pending_notice' ) ); ?>"
class="nav-tab <?php echo $active_tab === 'pending_notice' ? 'nav-tab-active' : ''; ?>">
<?php esc_html_e( 'Awaiting Notice', 'woo-cleanup' ); ?>
<span class="woo-cleanup-badge"><?php echo count( $pending_users ); ?></span>
</a>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=woo-cleanup&tab=notice_sent' ) ); ?>"
class="nav-tab <?php echo $active_tab === 'notice_sent' ? 'nav-tab-active' : ''; ?>">
<?php esc_html_e( 'Grace Period (Pending Deletion)', 'woo-cleanup' ); ?>
<span class="woo-cleanup-badge woo-cleanup-badge--warning"><?php echo count( $noticed_users ); ?></span>
</a>
</h2>
<?php if ( empty( $users_to_show ) ) : ?>
<div class="woo-cleanup-empty">
<?php if ( ! $last_run ) : ?>
<p><?php esc_html_e( 'No flagged accounts yet. The daily cron has not run. Click "Run Cleanup Now" to scan immediately.', 'woo-cleanup' ); ?></p>
<?php else : ?>
<p><?php esc_html_e( 'No accounts in this category.', 'woo-cleanup' ); ?></p>
<?php endif; ?>
</div>
<?php else : ?>
<table class="wp-list-table widefat fixed striped woo-cleanup-table">
<thead>
<tr>
<th><?php esc_html_e( 'User', 'woo-cleanup' ); ?></th>
<th><?php esc_html_e( 'Email', 'woo-cleanup' ); ?></th>
<th><?php esc_html_e( 'Registered', 'woo-cleanup' ); ?></th>
<th><?php esc_html_e( 'Last Order', 'woo-cleanup' ); ?></th>
<?php if ( $active_tab === 'pending_notice' ) : ?>
<th><?php esc_html_e( 'Inactive Since', 'woo-cleanup' ); ?></th>
<?php else : ?>
<th><?php esc_html_e( 'Notice Sent', 'woo-cleanup' ); ?></th>
<th><?php esc_html_e( 'Deletes On', 'woo-cleanup' ); ?></th>
<th><?php esc_html_e( 'Days Left', 'woo-cleanup' ); ?></th>
<?php endif; ?>
<th><?php esc_html_e( 'Actions', 'woo-cleanup' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $users_to_show as $user ) :
$user_id = (int) $user->ID;
$inactive_since = (int) get_user_meta( $user_id, WooCleanup::META_INACTIVE_SINCE, true );
$notice_ts = (int) get_user_meta( $user_id, WooCleanup::META_NOTICE_SENT, true );
$delete_ts = $notice_ts + ( $grace_days * DAY_IN_SECONDS );
$days_left = max( 0, (int) ceil( ( $delete_ts - time() ) / DAY_IN_SECONDS ) );
$last_order = WooCleanup::get_last_order_date( $user_id );
$edit_url = get_edit_user_link( $user_id );
$row_class = ( $active_tab === 'notice_sent' && $days_left <= 2 ) ? 'woo-cleanup-row--urgent' : '';
?>
<tr class="<?php echo esc_attr( $row_class ); ?>">
<td>
<a href="<?php echo esc_url( $edit_url ); ?>" target="_blank">
<?php echo esc_html( $user->display_name ); ?>
</a>
<br><small style="color:#888">#<?php echo $user_id; ?></small>
</td>
<td><?php echo esc_html( $user->user_email ); ?></td>
<td><?php echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $user->user_registered ) ) ); ?></td>
<td><?php echo $last_order ? esc_html( $last_order ) : '<em style="color:#aaa">' . esc_html__( 'Never', 'woo-cleanup' ) . '</em>'; ?></td>
<?php if ( $active_tab === 'pending_notice' ) : ?>
<td><?php echo $inactive_since ? esc_html( date_i18n( get_option( 'date_format' ), $inactive_since ) ) : '—'; ?></td>
<?php else : ?>
<td><?php echo $notice_ts ? esc_html( date_i18n( get_option( 'date_format' ), $notice_ts ) ) : '—'; ?></td>
<td><?php echo $notice_ts ? esc_html( date_i18n( get_option( 'date_format' ), $delete_ts ) ) : '—'; ?></td>
<td>
<?php if ( $days_left <= 1 ) : ?>
<span class="woo-cleanup-days-urgent"><?php echo $days_left === 0 ? esc_html__( 'Today', 'woo-cleanup' ) : esc_html( $days_left . ' ' . __( 'day', 'woo-cleanup' ) ); ?></span>
<?php else : ?>
<?php echo esc_html( $days_left . ' ' . __( 'days', 'woo-cleanup' ) ); ?>
<?php endif; ?>
</td>
<?php endif; ?>
<td class="woo-cleanup-actions">
<?php if ( $active_tab === 'pending_notice' ) : ?>
<!-- Send notice now -->
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="display:inline">
<?php wp_nonce_field( 'woo_cleanup_send_notice_' . $user_id, '_wpnonce' ); ?>
<input type="hidden" name="action" value="woo_cleanup_send_notice">
<input type="hidden" name="user_id" value="<?php echo $user_id; ?>">
<button type="submit" class="button button-small">
<?php esc_html_e( 'Send Notice', 'woo-cleanup' ); ?>
</button>
</form>
<?php endif; ?>
<!-- Delete now -->
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"
style="display:inline"
onsubmit="return confirm('<?php echo esc_js( sprintf( __( 'Permanently delete %s? This cannot be undone.', 'woo-cleanup' ), $user->display_name ) ); ?>')">
<?php wp_nonce_field( 'woo_cleanup_delete_user_' . $user_id, '_wpnonce' ); ?>
<input type="hidden" name="action" value="woo_cleanup_delete_user">
<input type="hidden" name="user_id" value="<?php echo $user_id; ?>">
<button type="submit" class="button button-small woo-cleanup-btn-delete">
<?php esc_html_e( 'Delete Now', 'woo-cleanup' ); ?>
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div><!-- .wrap -->
<?php
}
// =========================================================================
// Settings Page
// =========================================================================
public function render_settings_page(): void {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( esc_html__( 'You do not have permission to view this page.', 'woo-cleanup' ) );
}
$options = $this->plugin->get_options();
?>
<div class="wrap">
<h1><?php esc_html_e( 'WooCleanup Settings', 'woo-cleanup' ); ?></h1>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<?php wp_nonce_field( 'woo_cleanup_save_settings', '_wpnonce' ); ?>
<input type="hidden" name="action" value="woo_cleanup_save_settings">
<table class="form-table" role="presentation">
<!-- Enable / Disable -->
<tr>
<th scope="row"><?php esc_html_e( 'Enable Cleanup', 'woo-cleanup' ); ?></th>
<td>
<label>
<input type="checkbox" name="woo_cleanup[enabled]" value="1"
<?php checked( 1, (int) $options['enabled'] ); ?>>
<?php esc_html_e( 'Run the daily cleanup cron', 'woo-cleanup' ); ?>
</label>
</td>
</tr>
<!-- Inactivity Period -->
<tr>
<th scope="row">
<label for="wc_inactivity_months">
<?php esc_html_e( 'Inactivity Period (months)', 'woo-cleanup' ); ?>
</label>
</th>
<td>
<input type="number" id="wc_inactivity_months"
name="woo_cleanup[inactivity_months]"
value="<?php echo esc_attr( $options['inactivity_months'] ); ?>"
min="1" max="120" style="width:80px">
<p class="description">
<?php esc_html_e( 'Customers with no completed/processing order in this many months will be flagged. Default: 18.', 'woo-cleanup' ); ?>
</p>
</td>
</tr>
<!-- Grace Period -->
<tr>
<th scope="row">
<label for="wc_grace_days">
<?php esc_html_e( 'Grace Period (days)', 'woo-cleanup' ); ?>
</label>
</th>
<td>
<input type="number" id="wc_grace_days"
name="woo_cleanup[grace_days]"
value="<?php echo esc_attr( $options['grace_days'] ); ?>"
min="1" max="365" style="width:80px">
<p class="description">
<?php esc_html_e( 'Days between the notice email and account deletion. Default: 7.', 'woo-cleanup' ); ?>
</p>
</td>
</tr>
<!-- Email Subject -->
<tr>
<th scope="row">
<label for="wc_email_subject">
<?php esc_html_e( 'Notice Email Subject', 'woo-cleanup' ); ?>
</label>
</th>
<td>
<input type="text" id="wc_email_subject"
name="woo_cleanup[email_subject]"
value="<?php echo esc_attr( $options['email_subject'] ); ?>"
class="large-text">
<p class="description">
<?php esc_html_e( 'Available placeholders: {site_name}, {username}, {email}, {deletion_date}, {grace_days}', 'woo-cleanup' ); ?>
</p>
</td>
</tr>
<!-- Email Body -->
<tr>
<th scope="row">
<label for="wc_email_body">
<?php esc_html_e( 'Notice Email Body', 'woo-cleanup' ); ?>
</label>
</th>
<td>
<textarea id="wc_email_body"
name="woo_cleanup[email_body]"
rows="12" class="large-text"><?php echo esc_textarea( $options['email_body'] ); ?></textarea>
<p class="description">
<?php esc_html_e( 'Plain text. Available placeholders: {site_name}, {username}, {email}, {deletion_date}, {grace_days}. HTML is supported.', 'woo-cleanup' ); ?>
</p>
</td>
</tr>
</table>
<h2><?php esc_html_e( 'Preview Placeholders', 'woo-cleanup' ); ?></h2>
<table class="widefat" style="max-width:500px">
<thead><tr><th><?php esc_html_e( 'Placeholder', 'woo-cleanup' ); ?></th><th><?php esc_html_e( 'Replaced With', 'woo-cleanup' ); ?></th></tr></thead>
<tbody>
<tr><td><code>{site_name}</code></td><td><?php echo esc_html( get_bloginfo( 'name' ) ); ?></td></tr>
<tr><td><code>{username}</code></td><td><?php esc_html_e( "Customer's display name", 'woo-cleanup' ); ?></td></tr>
<tr><td><code>{email}</code></td><td><?php esc_html_e( "Customer's email address", 'woo-cleanup' ); ?></td></tr>
<tr><td><code>{deletion_date}</code></td><td><?php esc_html_e( 'Calculated deletion date', 'woo-cleanup' ); ?></td></tr>
<tr><td><code>{grace_days}</code></td><td><?php echo esc_html( $options['grace_days'] ); ?></td></tr>
</tbody>
</table>
<p style="margin-top:20px">
<?php submit_button( __( 'Save Settings', 'woo-cleanup' ), 'primary', 'submit', false ); ?>
&nbsp;
<a href="<?php echo esc_url( admin_url( 'admin.php?page=woo-cleanup' ) ); ?>" class="button button-secondary">
<?php esc_html_e( 'View Flagged Accounts', 'woo-cleanup' ); ?>
</a>
</p>
</form>
</div>
<?php
}
// =========================================================================
// Form Handlers (admin-post.php targets)
// =========================================================================
public function save_settings(): void {
if ( ! current_user_can( 'manage_woocommerce' )
|| ! check_admin_referer( 'woo_cleanup_save_settings' ) ) {
wp_die( esc_html__( 'Security check failed.', 'woo-cleanup' ) );
}
$raw = isset( $_POST['woo_cleanup'] ) ? (array) $_POST['woo_cleanup'] : [];
$options = [
'enabled' => ! empty( $raw['enabled'] ) ? 1 : 0,
'inactivity_months' => max( 1, absint( $raw['inactivity_months'] ?? 18 ) ),
'grace_days' => max( 1, absint( $raw['grace_days'] ?? 7 ) ),
'email_subject' => sanitize_text_field( $raw['email_subject'] ?? '' ),
'email_body' => wp_kses_post( $raw['email_body'] ?? '' ),
];
update_option( 'woo_cleanup_options', $options );
wp_safe_redirect( add_query_arg(
[ 'page' => 'woo-cleanup-settings', 'message' => 'saved' ],
admin_url( 'admin.php' )
) );
exit;
}
public function handle_manual_run(): void {
if ( ! current_user_can( 'manage_woocommerce' )
|| ! check_admin_referer( 'woo_cleanup_manual_run' ) ) {
wp_die( esc_html__( 'Security check failed.', 'woo-cleanup' ) );
}
$this->plugin->run_cleanup();
wp_safe_redirect( add_query_arg(
[ 'page' => 'woo-cleanup', 'message' => 'ran' ],
admin_url( 'admin.php' )
) );
exit;
}
public function handle_send_notice(): void {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( esc_html__( 'Security check failed.', 'woo-cleanup' ) );
}
$user_id = absint( $_POST['user_id'] ?? 0 );
if ( ! $user_id || ! check_admin_referer( 'woo_cleanup_send_notice_' . $user_id ) ) {
wp_die( esc_html__( 'Security check failed.', 'woo-cleanup' ) );
}
$options = $this->plugin->get_options();
$this->plugin->send_notice( $user_id, absint( $options['grace_days'] ), $options );
update_user_meta( $user_id, WooCleanup::META_NOTICE_SENT, time() );
wp_safe_redirect( add_query_arg(
[ 'page' => 'woo-cleanup', 'message' => 'noticed', 'uid' => $user_id ],
admin_url( 'admin.php' )
) );
exit;
}
public function handle_delete_user(): void {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( esc_html__( 'Security check failed.', 'woo-cleanup' ) );
}
$user_id = absint( $_POST['user_id'] ?? 0 );
if ( ! $user_id || ! check_admin_referer( 'woo_cleanup_delete_user_' . $user_id ) ) {
wp_die( esc_html__( 'Security check failed.', 'woo-cleanup' ) );
}
// Double-check: never delete admins via this form.
$user = get_userdata( $user_id );
if ( $user && in_array( 'administrator', (array) $user->roles, true ) ) {
wp_die( esc_html__( 'Cannot delete administrator accounts.', 'woo-cleanup' ) );
}
$this->plugin->delete_customer( $user_id );
wp_safe_redirect( add_query_arg(
[ 'page' => 'woo-cleanup', 'message' => 'deleted' ],
admin_url( 'admin.php' )
) );
exit;
}
// =========================================================================
// Admin Notices
// =========================================================================
public function show_admin_notices(): void {
$screen = get_current_screen();
if ( ! $screen || strpos( $screen->id, 'woo-cleanup' ) === false ) {
return;
}
$message = $_GET['message'] ?? '';
$uid = absint( $_GET['uid'] ?? 0 );
switch ( $message ) {
case 'saved':
echo '<div class="notice notice-success is-dismissible"><p>'
. esc_html__( 'Settings saved.', 'woo-cleanup' )
. '</p></div>';
break;
case 'ran':
echo '<div class="notice notice-success is-dismissible"><p>'
. esc_html__( 'Cleanup routine completed. The account list has been updated.', 'woo-cleanup' )
. '</p></div>';
break;
case 'noticed':
$user = $uid ? get_userdata( $uid ) : null;
echo '<div class="notice notice-success is-dismissible"><p>'
. esc_html( sprintf(
__( 'Notice email sent to %s.', 'woo-cleanup' ),
$user ? $user->display_name : "user #{$uid}"
) )
. '</p></div>';
break;
case 'deleted':
echo '<div class="notice notice-success is-dismissible"><p>'
. esc_html__( 'Account deleted.', 'woo-cleanup' )
. '</p></div>';
break;
}
}
// =========================================================================
// Inline Styles (small footprint; avoids an extra HTTP request)
// =========================================================================
private function output_admin_styles(): void {
?>
<style>
.woo-cleanup-summary {
display: flex;
align-items: center;
gap: 24px;
background: #fff;
border: 1px solid #e2e4e7;
border-radius: 4px;
padding: 16px 20px;
margin: 16px 0;
}
.woo-cleanup-stat {
text-align: center;
}
.woo-cleanup-stat .count {
display: block;
font-size: 2em;
font-weight: 700;
line-height: 1;
color: #2271b1;
}
.woo-cleanup-stat--warning .count {
color: #d63638;
}
.woo-cleanup-stat .label {
display: block;
font-size: 0.8em;
color: #646970;
margin-top: 4px;
}
.woo-cleanup-meta {
font-size: 0.85em;
color: #646970;
}
.woo-cleanup-badge {
display: inline-block;
background: #2271b1;
color: #fff;
border-radius: 10px;
padding: 1px 7px;
font-size: 0.8em;
margin-left: 4px;
vertical-align: middle;
}
.woo-cleanup-badge--warning {
background: #d63638;
}
.woo-cleanup-table th,
.woo-cleanup-table td {
vertical-align: middle;
}
.woo-cleanup-actions {
white-space: nowrap;
}
.woo-cleanup-btn-delete {
color: #d63638 !important;
border-color: #d63638 !important;
}
.woo-cleanup-btn-delete:hover {
background: #d63638 !important;
color: #fff !important;
}
.woo-cleanup-row--urgent td {
background-color: #fff5f5 !important;
}
.woo-cleanup-days-urgent {
color: #d63638;
font-weight: 700;
}
.woo-cleanup-empty {
background: #fff;
border: 1px solid #e2e4e7;
border-radius: 4px;
padding: 40px;
text-align: center;
color: #646970;
margin-top: 16px;
}
</style>
<?php
}
}