PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 2.3.0
Subscriptions for WooCommerce with Stripe Recurring Payments v2.3.0
2.4.0 2.3.0 2.2.0 2.1.0 2.0.0 1.11.2 1.11.1 1.11.0 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.6 1.9.5 trunk 1.3.0 1.3.1 1.3.2 1.4.0 All 65 releases
← All changes | includes/Admin/views/subscription-list.php +515 -234 1.9.5 → 2.3.0 View file →
@@ -4,9 +4,8 @@
4 4 *
5 5 * @package SpringDevs\Subscription\Admin
6 6 */
7 7
8 -// Exit if accessed directly.
9 8 if ( ! defined( 'ABSPATH' ) ) {
10 9 exit;
11 10 }
12 11
@@ -12,265 +11,547 @@
12 11
13 12 if ( ! isset( $date_filter ) ) {
14 13 $date_filter = '';
15 14 }
15 +if ( ! isset( $renewal_due ) ) {
16 + $renewal_due = 0;
17 +}
16 18
17 -// Determine if filters are active
18 -$filters_active = ! empty( $status ) || ! empty( $date_filter ) || ! empty( $search );
19 -$months = array();
19 +$filters_active = ! empty( $status ) || ! empty( $date_filter ) || ! empty( $search ) || ! empty( $renewal_due );
20 +
21 +// The last twelve of the store's months, counted back from the 1st: stepping
22 +// back from today skips a month on the 29th–31st (31 Oct − 1 month is 1 Oct).
23 +// The store's timezone, because the list filters on the local post date.
24 +$months = array();
25 +$this_month = ( new DateTimeImmutable( 'now', wp_timezone() ) )->modify( 'first day of this month' )->setTime( 0, 0 );
20 26 for ( $i = 0; $i < 12; $i++ ) {
21 27 $month = strtotime( "-$i month" );
22 - $months[ date( 'Y-m', $month ) ] = date( 'F Y', $month );
28 + $months[ gmdate( 'Y-m', $month ) ] = gmdate( 'F Y', $month );
23 29 }
24 30 ?>
25 31 <div class="wp-subscription-admin-content list-page">
26 - <div class="wp-subscription-list-title"><h1 class="wp-heading-inline">Subscriptions</h1></div>
27 -
32 +
33 + <?php
34 + // Getting started ("Welcome to WPSubscription") card hidden for now — re-enable later.
35 + // require __DIR__ . '/subscription-gsc.php';
36 + ?>
37 +
38 + <?php
39 + wpsubs_render_page_header(
40 + array(
41 + 'title' => __( 'Subscriptions', 'subscription' ),
42 + 'description' => __( 'Manage all your WooCommerce subscriptions.', 'subscription' ),
43 + )
44 + );
45 + ?>
46 +
28 47 <form method="post" id="subscriptions-form">
29 - <div class="wp-subscription-list-header">
30 - <div class="wp-subscription-filters">
31 - <?php wp_nonce_field( 'subscrpt_list_action' ); ?>
32 - <input type="hidden" name="page" value="wp-subscription" />
33 - <select name="subscrpt_status" value="<?php echo esc_attr( $status ); ?>">
34 - <option value=""><?php esc_html_e( 'All Status', 'subscription' ); ?></option>
35 - <option value="active" <?php selected( $status, 'active' ); ?>><?php esc_html_e( 'Active', 'subscription' ); ?></option>
36 - <option value="pending" <?php selected( $status, 'pending' ); ?>><?php esc_html_e( 'Pending', 'subscription' ); ?></option>
37 - <option value="cancelled" <?php selected( $status, 'cancelled' ); ?>><?php esc_html_e( 'Cancelled', 'subscription' ); ?></option>
38 - <option value="expired" <?php selected( $status, 'expired' ); ?>><?php esc_html_e( 'Expired', 'subscription' ); ?></option>
39 - <option value="draft" <?php selected( $status, 'draft' ); ?>><?php esc_html_e( 'Draft', 'subscription' ); ?></option>
40 - <option value="trash" <?php selected( $status, 'trash' ); ?>><?php esc_html_e( 'Trash', 'subscription' ); ?></option>
41 - </select>
42 - <select name="date_filter" value="<?php echo esc_attr( $date_filter ); ?>">
43 - <option value=""><?php esc_html_e( 'All Dates', 'subscription' ); ?></option>
44 - <?php foreach ( $months as $val => $label ) : ?>
45 - <option value="<?php echo esc_attr( $val ); ?>" <?php selected( $date_filter, $val ); ?>><?php echo esc_html( $label ); ?></option>
46 - <?php endforeach; ?>
47 - </select>
48 - <input type="search" name="s" value="<?php echo esc_attr( $search ); ?>" placeholder="<?php esc_attr_e( 'Search by subscription ID...', 'subscription' ); ?>" />
49 - <select name="per_page">
50 - <?php foreach ( array( 10, 20, 50, 100 ) as $n ) : ?>
51 - <option value="<?php echo (int) $n; ?>" <?php selected( isset( $_GET['per_page'] ) ? intval( wp_unslash( $_GET['per_page'] ) ) : 20, $n ); ?>><?php echo (int) $n; ?> per page</option>
52 - <?php endforeach; ?>
53 - </select>
54 - <button type="submit" name="filter_action" value="filter" class="button">Search</button>
55 - <?php if ( $filters_active ) : ?>
56 - <a href="<?php echo esc_url( remove_query_arg( array( 'subscrpt_status', 'date_filter', 's', 'title', 'paged' ) ) ); ?>" class="button">Reset</a>
57 - <?php endif; ?>
58 - <?php if ( $filters_active ) : ?>
59 - <span>Filters applied</span>
60 - <?php endif; ?>
48 + <?php wp_nonce_field( 'subscrpt_list_action' ); ?>
49 + <input type="hidden" name="page" value="wp-subscription-list" />
50 +
51 + <!-- Toolbar -->
52 + <div class="wpsubs-toolbar">
53 +
54 + <?php
55 + wpsubs_render_adv_select(
56 + array(
57 + 'name' => 'subscrpt_status',
58 + 'placeholder' => __( 'All Status', 'subscription' ),
59 + 'value' => $status,
60 + 'options' => array(
61 + array(
62 + 'value' => 'active',
63 + 'label' => __( 'Active', 'subscription' ),
64 + ),
65 + array(
66 + 'value' => 'on_hold',
67 + 'label' => __( 'On Hold', 'subscription' ),
68 + ),
69 + array(
70 + 'value' => 'pending',
71 + 'label' => __( 'Pending', 'subscription' ),
72 + ),
73 + array(
74 + 'value' => 'pe_cancelled',
75 + 'label' => __( 'Pending Cancellation', 'subscription' ),
76 + ),
77 + array(
78 + 'value' => 'cancelled',
79 + 'label' => __( 'Cancelled', 'subscription' ),
80 + ),
81 + array(
82 + 'value' => 'expired',
83 + 'label' => __( 'Expired', 'subscription' ),
84 + ),
85 + array(
86 + 'value' => 'completed',
87 + 'label' => __( 'Completed', 'subscription' ),
88 + ),
89 + array(
90 + 'value' => 'trash',
91 + 'label' => __( 'Trash', 'subscription' ),
92 + ),
93 + ),
94 + )
95 + );
96 + ?>
97 +
98 + <?php
99 + $date_options = array();
100 + foreach ( $months as $val => $label ) {
101 + $date_options[] = array(
102 + 'value' => $val,
103 + 'label' => $label,
104 + );
105 + }
106 + wpsubs_render_adv_select(
107 + array(
108 + 'name' => 'date_filter',
109 + 'placeholder' => __( 'All Dates', 'subscription' ),
110 + 'value' => $date_filter,
111 + 'options' => $date_options,
112 + )
113 + );
114 + ?>
115 +
116 + <?php
117 + // Next-renewal window. The Overview's "Renewals due" figure opens the
118 + // list with 7 chosen; a window reached by URL is offered too, so the
119 + // dropdown always shows what is applied.
120 + $renewal_windows = array_unique( array_merge( array( 7, 14, 30 ), $renewal_due ? array( $renewal_due ) : array() ) );
121 + sort( $renewal_windows );
122 + $renewal_options = array();
123 + foreach ( $renewal_windows as $days ) {
124 + $renewal_options[] = array(
125 + 'value' => (string) $days,
126 + /* translators: %d: number of days ahead. */
127 + 'label' => sprintf( _n( 'Due in %d day', 'Due in %d days', $days, 'subscription' ), $days ),
128 + );
129 + }
130 + wpsubs_render_adv_select(
131 + array(
132 + 'name' => 'renewal_due',
133 + 'placeholder' => __( 'All Renewals', 'subscription' ),
134 + 'value' => $renewal_due ? (string) $renewal_due : '',
135 + 'options' => $renewal_options,
136 + )
137 + );
138 + ?>
139 +
140 + <div class="wpsubs-search">
141 + <div class="wpsubs-input-wrap wpsubs-input-wrap--icon-l">
142 + <svg class="wpsubs-input-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z"/></svg>
143 + <input type="search" name="s" value="<?php echo esc_attr( $search ); ?>" placeholder="<?php esc_attr_e( 'Search subscriptions...', 'subscription' ); ?>" class="wpsubs-input" />
144 + </div>
61 145 </div>
62 -
63 - <?php if ( ! empty( $subscriptions ) ) : ?>
64 - <div class="wp-subscription-bulk-actions">
65 - <select name="action">
66 - <option value="-1"><?php esc_html_e( 'Bulk Actions', 'subscription' ); ?></option>
67 - <?php if ( $status === 'trash' ) : ?>
68 - <option value="restore"><?php esc_html_e( 'Restore', 'subscription' ); ?></option>
69 - <option value="delete"><?php esc_html_e( 'Delete Permanently', 'subscription' ); ?></option>
70 - <?php else : ?>
71 - <option value="trash"><?php esc_html_e( 'Move to Trash', 'subscription' ); ?></option>
72 - <?php endif; ?>
73 - </select>
74 - <input type="submit" name="bulk_action" value="<?php esc_attr_e( 'Apply', 'subscription' ); ?>" class="button action">
75 - <?php if ( $status === 'trash' && ! empty( $subscriptions ) ) : ?>
146 +
147 + <button type="submit" name="filter_action" value="filter" class="wpsubs-btn wpsubs-btn--outline">
148 + <?php esc_html_e( 'Filter', 'subscription' ); ?>
149 + </button>
150 +
151 + <?php if ( $filters_active ) : ?>
152 + <a href="<?php echo esc_url( remove_query_arg( array( 'subscrpt_status', 'date_filter', 's', 'title', 'paged', 'renewal_due' ) ) ); ?>" class="wpsubs-btn wpsubs-btn--outline">
153 + <?php esc_html_e( 'Clear', 'subscription' ); ?>
154 + </a>
155 + <?php endif; ?>
156 +
157 + <div class="wpsubs-toolbar__spacer"></div>
158 +
159 + <?php // Page size and bulk actions wrap as one right-aligned group, never one without the other. ?>
160 + <div style="display:flex;flex-wrap:wrap;gap:8px;margin-inline-start:auto;">
161 + <?php
162 + $current_per_page = isset( $_GET['per_page'] ) ? intval( wp_unslash( $_GET['per_page'] ) ) : 20;
163 + wpsubs_render_per_page_select(
164 + array(
165 + 'name' => 'per_page',
166 + 'value' => (string) $current_per_page,
167 + 'align' => 'right',
168 + 'id' => 'wpsubs-per-page-select',
169 + )
170 + );
171 + ?>
172 +
173 + <?php
174 + // Bulk action advanced-select + hidden submit.
175 + $bulk_options = 'trash' === $status
176 + ? array(
177 + array(
178 + 'value' => 'restore',
179 + 'label' => __( 'Restore selected', 'subscription' ),
180 + ),
181 + array(
182 + 'value' => 'delete',
183 + 'label' => __( 'Delete permanently', 'subscription' ),
184 + 'danger' => true,
185 + 'confirm' => __( 'Delete selected subscriptions permanently? This cannot be undone.', 'subscription' ),
186 + ),
187 + )
188 + : array(
189 + array(
190 + 'value' => 'trash',
191 + 'label' => __( 'Move to trash', 'subscription' ),
192 + 'danger' => true,
193 + 'confirm' => __( 'Move selected subscriptions to trash?', 'subscription' ),
194 + ),
195 + );
196 +
197 + wpsubs_render_adv_select(
198 + array(
199 + 'name' => 'action',
200 + 'placeholder' => __( 'Select Action', 'subscription' ),
201 + 'value' => '-1',
202 + 'options' => $bulk_options,
203 + 'id' => 'wpsubs-bulk-action-select',
204 + 'align' => 'right',
205 + )
206 + );
207 + ?>
208 + <button type="submit" name="bulk_action" value="1" id="wpsubs-bulk-action-submit" style="display:none;" aria-hidden="true"></button>
209 +
210 + <?php
211 + if ( 'trash' === $status && ! empty( $subscriptions ) ) :
212 + $empty_trash_url = wp_nonce_url( admin_url( 'admin.php?page=wp-subscription-list&action=clean_trash&sub_id=all' ), 'wpsubs_action_clean_trash' );
213 + ?>
214 + <a href="<?php echo esc_url( $empty_trash_url ); ?>" class="wpsubs-btn wpsubs-btn--danger" onclick="return confirm('<?php esc_attr_e( 'Permanently delete all trash items? This cannot be undone.', 'subscription' ); ?>')">
215 + <?php esc_html_e( 'Empty Trash', 'subscription' ); ?>
216 + </a>
217 + <?php endif; ?>
218 + </div>
219 +
220 + </div><!-- /.wpsubs-toolbar -->
221 +
222 + <!-- Table card -->
223 + <h2 class="screen-reader-text"><?php esc_html_e( 'Subscriptions list', 'subscription' ); ?></h2>
224 + <div class="wpsubs-table-card">
225 + <table class="wpsubs-table">
226 + <thead>
227 + <tr>
228 + <th class="wpsubs-col--check">
229 + <input type="checkbox" id="cb-select-all" class="wpsubs-checkbox">
230 + </th>
231 + <th style="width:1%;white-space:nowrap;"><?php esc_html_e( 'Status', 'subscription' ); ?></th>
232 + <th><?php esc_html_e( 'Subscription', 'subscription' ); ?></th>
233 + <th><?php esc_html_e( 'Customer', 'subscription' ); ?></th>
234 + <th><?php esc_html_e( 'Amount', 'subscription' ); ?></th>
235 + <th><?php esc_html_e( 'Started', 'subscription' ); ?></th>
236 + <th><?php esc_html_e( 'Next Renewal', 'subscription' ); ?></th>
237 + <th class="wpsubs-col--actions"><?php esc_html_e( 'Actions', 'subscription' ); ?></th>
238 + </tr>
239 + </thead>
240 + <tbody>
241 +
242 + <?php if ( ! empty( $subscriptions ) ) : ?>
76 243 <?php
77 - $nonce_action = 'wpsubs_action_clean_trash';
78 - $empty_trash_url = wp_nonce_url( admin_url( 'admin.php?page=wp-subscription&action=clean_trash&sub_id=all' ), $nonce_action );
79 - ?>
80 - <a href="<?php echo esc_url( $empty_trash_url ); ?>"
81 - class="button button-link-delete"
82 - onclick="return confirm('<?php esc_attr_e( 'Are you sure you want to permanently delete all items in trash? This action cannot be undone.', 'subscription' ); ?>')">
83 - <?php esc_html_e( 'Empty Trash', 'subscription' ); ?>
84 - </a>
85 - <?php endif; ?>
86 - </div>
87 - <?php endif; ?>
88 - </div>
89 -
90 - <h2 class="screen-reader-text">Subscriptions list</h2>
91 - <table class="wp-list-table widefat fixed striped wp-subscription-modern-table">
92 - <thead>
93 - <tr>
94 - <th style="width:20px;"><input type="checkbox" id="cb-select-all-1"></th>
95 - <th style="width:180px;">ID</th>
96 - <th style="min-width:320px;">Title</th>
97 - <th style="width:180px;">Customer</th>
98 - <th style="width:100px;">Start Date</th>
99 - <th style="width:100px;">Renewal Date</th>
100 - <th style="width:100px;">Status</th>
101 - <th style="width:80px;">Actions</th>
102 - </tr>
103 - </thead>
104 - <tbody>
105 - <?php if ( ! empty( $subscriptions ) ) : ?>
106 - <?php
107 - foreach ( $subscriptions as $subscription ) :
108 - $subscription_id = $subscription->ID;
109 - $subscription_data = SpringDevs\Subscription\Illuminate\Helper::get_subscription_data( $subscription_id );
244 + foreach ( $subscriptions as $subscription ) :
245 + $subscription_id = $subscription->ID;
246 + $subscription_data = SpringDevs\Subscription\Illuminate\Helper::get_subscription_data( $subscription_id );
110 247
111 - $subscrpt_status = $subscription_data['status'] ?? '';
112 - $subscrpt_status = empty( $subscrpt_status ) ? get_post_status( $subscription_id ) : $subscrpt_status;
248 + $subscrpt_status = $subscription_data['status'] ?? '';
249 + $subscrpt_status = empty( $subscrpt_status ) ? get_post_status( $subscription_id ) : $subscrpt_status;
113 250
114 - $order_id = $subscription_data['order']['order_id'] ?? 0;
115 - $order_item_id = $subscription_data['order']['order_item_id'] ?? 0;
116 - $order = $order_id ? wc_get_order( $order_id ) : null;
117 - $order_item = $order ? $order->get_item( $order_item_id ) : null;
118 - $product_name = $order_item ? $order_item->get_name() : '-';
251 + $order_id = $subscription_data['order']['order_id'] ?? 0;
252 + $order_item_id = $subscription_data['order']['order_item_id'] ?? 0;
253 + $order = $order_id ? wc_get_order( $order_id ) : null;
254 + $order_item = $order ? $order->get_item( $order_item_id ) : null;
255 + $product_name = $order_item ? $order_item->get_name() : '-';
119 256
120 - $customer = $order ? $order->get_formatted_billing_full_name() : '-';
121 - $customer_id = $order ? $order->get_customer_id() : 0;
122 - $customer_url = $customer_id ? admin_url( 'user-edit.php?user_id=' . $customer_id ) : '';
123 - $customer_email = $order ? $order->get_billing_email() : '';
257 + $product_id = $subscription_data['product']['variation_id'] ?: ( $subscription_data['product']['product_id'] ?? 0 );
258 + $product_url = $product_id ? get_edit_post_link( $product_id ) : '';
124 259
125 - $start_date = $subscription_data['start_date'] ? strtotime( $subscription_data['start_date'] ) : 0;
126 - $renewal_date = $subscription_data['next_date'] ? strtotime( $subscription_data['next_date'] ) : 0;
127 - $status_obj = get_post_status_object( get_post_status( $subscription->ID ) );
260 + $customer = $order ? $order->get_formatted_billing_full_name() : '-';
261 + $customer_id = $order ? $order->get_customer_id() : 0;
262 + $customer_url = $customer_id ? admin_url( 'user-edit.php?user_id=' . $customer_id ) : '';
263 + $customer_email = $order ? $order->get_billing_email() : '';
128 264
129 - $is_trash = $subscription->post_status === 'trash';
265 + $start_date = $subscription_data['start_date'] ? strtotime( $subscription_data['start_date'] ) : 0;
266 + $renewal_date = $subscription_data['next_date'] && SpringDevs\Subscription\Illuminate\Helper::has_next_payment( $subscrpt_status ) ? strtotime( $subscription_data['next_date'] ) : 0;
130 267
131 - $is_grace_period = isset( $subscription_data['grace_period'] );
132 - $grace_remaining = $subscription_data['grace_period']['remaining_days'] ?? 0;
268 + $is_trash = 'trash' === $subscription->post_status;
269 + $is_grace_period = isset( $subscription_data['grace_period'] );
270 + $grace_remaining = $subscription_data['grace_period']['remaining_days'] ?? 0;
133 271
134 - // Build URLs
135 - $nonce_action = 'wpsubs_action_' . $subscription->ID;
136 - $view_subs_url = get_edit_post_link( $subscription->ID );
137 - $duplicate_subs_url = wp_nonce_url( admin_url( 'admin.php?page=wp-subscription&action=duplicate&sub_id=' . $subscription->ID ), $nonce_action );
138 - $trash_subs_url = wp_nonce_url( admin_url( 'admin.php?page=wp-subscription&action=trash&sub_id=' . $subscription->ID ), $nonce_action );
139 - $del_per_subs_url = wp_nonce_url( admin_url( 'admin.php?page=wp-subscription&action=delete&sub_id=' . $subscription->ID ), $nonce_action );
140 - $restore_subs_url = wp_nonce_url( admin_url( 'admin.php?page=wp-subscription&action=restore&sub_id=' . $subscription->ID ), $nonce_action );
141 - ?>
142 - <tr>
143 - <td><input type="checkbox" name="subscription_ids[]" value="<?php echo esc_attr( $subscription->ID ); ?>"></td>
144 - <td>
145 - <div class="wp-subscription-title-wrap">
146 - <a href="<?php echo esc_url( $view_subs_url ); ?>" class="subscrpt-id-link">
147 - #<?php echo esc_html( get_the_title( $subscription->ID ) ); ?>
148 - </a>
272 + // Amount + timing. Net of any discount that carries into renewals.
273 + $quantity = $order_item ? (int) $order_item->get_quantity() : 1;
274 + $amount_totals = SpringDevs\Subscription\Illuminate\Helper::get_subscription_display_totals( $subscription_id, $order_item );
275 + $price = '' !== ( $subscription_data['price'] ?? '' ) ? (float) $amount_totals['total'] : '';
276 + $price_full = (float) $amount_totals['full_excl'] + $amount_totals['tax'] + $amount_totals['discount_tax'];
277 + $has_discount = (bool) $amount_totals['has_discount'];
278 + $price_per_unit = '' !== $price ? $price / max( 1, $quantity ) : '';
279 + $timing_per = $subscription_data['schedule']['timing_per'] ?? '';
280 + $timing_option = $subscription_data['schedule']['timing_option'] ?? '';
281 + $timing_label = '';
282 + if ( $timing_per && $timing_option ) {
283 + $timing_unit = \SpringDevs\Subscription\Illuminate\Helper::get_typos( $timing_per, $timing_option );
284 + $timing_label = ( (int) $timing_per > 1 )
285 + ? $timing_per . ' ' . $timing_unit
286 + : $timing_unit;
287 + }
149 288
150 - <div class="wp-subscription-row-actions">
151 - <?php if ( ! $is_trash ) : ?>
152 - <a href="<?php echo esc_url( $view_subs_url ); ?>">View</a>
289 + // Avatar
290 + $name_parts = array_values( array_filter( explode( ' ', trim( $customer ) ) ) );
291 + $initials = '?';
292 + if ( $name_parts ) {
293 + $initials = '';
294 + foreach ( array_slice( $name_parts, 0, 2 ) as $part ) {
295 + $initials .= strtoupper( $part[0] );
296 + }
297 + }
298 + $color_slot = ord( strtolower( $initials[0] ?? 'a' ) ) % 8;
153 299
154 - <!-- <a href="<?php echo esc_url( $duplicate_subs_url ); ?>">Duplicate</a> -->
300 + // Action URLs
301 + $nonce_action = 'wpsubs_action_' . $subscription->ID;
302 + $view_url = admin_url( 'admin.php?page=wp-subscription-details&id=' . $subscription->ID );
303 + $trash_url = wp_nonce_url( admin_url( 'admin.php?page=wp-subscription-list&action=trash&sub_id=' . $subscription->ID ), $nonce_action );
304 + $delete_url = wp_nonce_url( admin_url( 'admin.php?page=wp-subscription-list&action=delete&sub_id=' . $subscription->ID ), $nonce_action );
305 + $restore_url = wp_nonce_url( admin_url( 'admin.php?page=wp-subscription-list&action=restore&sub_id=' . $subscription->ID ), $nonce_action );
155 306
156 - <a href="<?php echo esc_url( $trash_subs_url ); ?>" onclick="return confirm('<?php esc_attr_e( 'Move this subscription to trash?', 'subscription' ); ?>')">Trash</a>
307 + // Status badge
308 + $badge_mod_map = array(
309 + 'active' => 'active',
310 + 'pending' => 'pending',
311 + 'pe_cancelled' => 'pending-cancel',
312 + 'cancelled' => 'cancelled',
313 + 'expired' => 'expired',
314 + 'completed' => 'completed',
315 + 'draft' => 'draft',
316 + 'trash' => 'trash',
317 + );
318 + $badge_mod = $badge_mod_map[ $subscrpt_status ] ?? 'expired';
319 + $verbose_status = SpringDevs\Subscription\Illuminate\Helper::get_verbose_status( $subscrpt_status );
157 320
158 - <?php else : ?>
159 - <a href="<?php echo esc_url( $restore_subs_url ); ?>">Restore</a>
321 + if ( $is_grace_period && $grace_remaining > 0 ) {
322 + $badge_mod = 'active';
323 + $verbose_status = __( 'Active', 'subscription' );
324 + }
325 + ?>
326 + <tr>
327 + <!-- Checkbox -->
328 + <td class="wpsubs-col--check">
329 + <input type="checkbox" name="subscription_ids[]" value="<?php echo esc_attr( $subscription->ID ); ?>" class="wpsubs-checkbox wpsubs-row-check">
330 + </td>
160 331
161 - <a href="<?php echo esc_url( $del_per_subs_url ); ?>" onclick="return confirm('<?php esc_attr_e( 'Delete this subscription permanently? This action cannot be undone.', 'subscription' ); ?>')" style="color:#d93025;">Delete Permanently</a>
332 + <!-- Status badge -->
333 + <td style="white-space:nowrap;">
334 + <span class="wpsubs-badge wpsubs-badge--<?php echo esc_attr( $badge_mod ); ?>">
335 + <?php echo esc_html( $verbose_status ); ?>
336 + <?php if ( $is_grace_period && $grace_remaining > 0 ) : ?>
337 + <span class="dashicons dashicons-warning" style="font-size:11px;width:11px;height:11px;color:#d97706;" title="<?php echo esc_attr( sprintf( /* translators: %d: number of days left in the grace period. */ __( '%d days remaining in grace period', 'subscription' ), $grace_remaining ) ); ?>"></span>
162 338 <?php endif; ?>
339 + </span>
340 + </td>
341 +
342 + <!-- Subscription: product name + subscription ID -->
343 + <td>
344 + <a href="<?php echo esc_url( $view_url ); ?>" class="wpsubs-cell-title" style="font-weight:600;"><?php echo esc_html( $product_name ); ?></a>
345 + <span class="wpsubs-cell-id">#<?php echo (int) $subscription->ID; ?></span>
346 + </td>
347 +
348 + <!-- Customer avatar + name + email -->
349 + <td>
350 + <div class="wpsubs-customer">
351 + <div class="wpsubs-avatar" data-color="<?php echo (int) $color_slot; ?>">
352 + <?php echo esc_html( $initials ); ?>
353 + </div>
354 + <div class="wpsubs-customer__info">
355 + <?php if ( $customer_url ) : ?>
356 + <a href="<?php echo esc_url( $customer_url ); ?>" class="wpsubs-customer__name"><?php echo esc_html( $customer ); ?></a>
357 + <?php else : ?>
358 + <span class="wpsubs-customer__name"><?php echo esc_html( $customer ); ?></span>
359 + <?php endif; ?>
360 + <?php if ( $customer_email ) : ?>
361 + <span class="wpsubs-customer__sub"><?php echo esc_html( $customer_email ); ?></span>
362 + <?php endif; ?>
363 + </div>
163 364 </div>
164 - </div>
165 - </td>
166 - <td style="min-width:320px;">
167 - <span><?php echo esc_html( $product_name ); ?></span>
168 - </td>
169 - <td>
170 - <?php if ( $customer_url ) : ?>
171 - <a href="<?php echo esc_url( $customer_url ); ?>" target="_blank"><?php echo esc_html( $customer ); ?></a>
172 - <?php else : ?>
173 - <?php echo esc_html( $customer ); ?>
174 - <?php endif; ?>
175 - <?php if ( $customer_email ) : ?>
176 - <div class="wp-subscription-customer-email"><?php echo esc_html( $customer_email ); ?></div>
177 - <?php endif; ?>
178 - </td>
179 - <td><?php echo $start_date ? esc_html( wp_date( 'F d, Y', $start_date ) ) : '-'; ?></td>
180 - <td><?php echo $renewal_date ? esc_html( wp_date( 'F d, Y', $renewal_date ) ) : '-'; ?></td>
181 - <td>
182 - <?php if ( $is_grace_period && $grace_remaining > 0 ) : ?>
183 - <span class="subscrpt-active grace-active">
184 - Active
365 + </td>
185 366
186 - <?php
187 - $grace_remaining_text = sprintf(
188 - // translators: Number of days remaining in grace period.
189 - __( '%d days remaining!', 'subscription' ),
190 - $grace_remaining
191 - );
192 - ?>
193 - <span class="grace-icon" data-tooltip="<?php echo esc_attr( $grace_remaining_text ); ?>">
194 - <span class="dashicons dashicons-warning"></span>
195 - </span>
196 - </span>
197 - <?php else : ?>
198 - <span class="subscrpt-<?php echo esc_attr( strtolower( $subscrpt_status ) ); ?>">
199 - <?php
200 - $verbose_status = SpringDevs\Subscription\Illuminate\Helper::get_verbose_status( $subscrpt_status );
201 - echo esc_html( strlen( $verbose_status ) > 9 ? substr( $verbose_status, 0, 9 ) . '...' : $verbose_status );
202 - ?>
203 - </span>
204 - <?php endif; ?>
205 - </td>
206 - <td>
207 - <a href="<?php echo esc_url( get_edit_post_link( $subscription->ID ) ); ?>" class="button button-small"><?php esc_html_e( 'Edit', 'subscription' ); ?></a>
208 - </td>
209 - </tr>
210 - <?php endforeach; ?>
211 - <?php else : ?>
212 - <tr>
213 - <td colspan="8" class="wp-subscription-list-empty">
214 - <?php esc_html_e( 'No subscriptions found.', 'subscription' ); ?>
215 - </td>
216 - </tr>
217 - <?php endif; ?>
218 - </tbody>
219 - </table>
220 -
221 - <?php if ( ! empty( $subscriptions ) ) : ?>
222 - <div class="tablenav bottom">
223 - <div class="alignleft actions bulkactions">
224 - <select name="action2">
225 - <option value="-1"><?php esc_html_e( 'Bulk Actions', 'subscription' ); ?></option>
226 - <?php if ( $status === 'trash' ) : ?>
227 - <option value="restore"><?php esc_html_e( 'Restore', 'subscription' ); ?></option>
228 - <option value="delete"><?php esc_html_e( 'Delete Permanently', 'subscription' ); ?></option>
229 - <?php else : ?>
230 - <option value="trash"><?php esc_html_e( 'Move to Trash', 'subscription' ); ?></option>
231 - <?php endif; ?>
232 - </select>
233 - <input type="submit" name="bulk_action2" value="<?php esc_attr_e( 'Apply', 'subscription' ); ?>" class="button action">
234 - </div>
235 - </div>
236 - <?php endif; ?>
237 - </form>
238 -
239 - <?php if ( $max_num_pages > 1 ) : ?>
240 - <div class="wp-subscription-pagination">
241 - <span class="total">Total <?php echo (int) $total; ?></span>
242 - <?php
243 - $base_url = remove_query_arg( 'paged' );
244 - $show_pages = $max_num_pages > 1 || $max_num_pages == 1;
245 - for ( $i = 1; $i <= $max_num_pages; $i++ ) :
246 - $url = add_query_arg(
367 + <!-- Subscription amount + timing -->
368 + <td style="white-space:nowrap;">
369 + <?php if ( '' !== $price ) : ?>
370 + <span class="wpsubs-cell-title" style="font-variant-numeric:tabular-nums;"><?php echo wp_kses_post( wc_price( $price ) ); ?></span>
371 + <?php if ( $has_discount ) : ?>
372 + <span class="wpsubs-cell-id"><del aria-hidden="true"><?php echo wp_kses_post( wc_price( $price_full ) ); ?></del></span>
373 + <?php endif; ?>
374 + <?php if ( $quantity > 1 ) : ?>
375 + <span class="wpsubs-cell-id"><?php echo wp_kses_post( wc_price( (float) $price_per_unit ) ); ?> &times; <?php echo (int) $quantity; ?></span>
376 + <?php endif; ?>
377 + <?php if ( $timing_label ) : ?>
378 + <span class="wpsubs-cell-id">/ <?php echo esc_html( $timing_label ); ?></span>
379 + <?php endif; ?>
380 + <?php else : ?>
381 + <span class="wpsubs-cell--muted">&#8212;</span>
382 + <?php endif; ?>
383 + </td>
384 +
385 + <!-- Start date -->
386 + <td class="wpsubs-col--nowrap">
387 + <?php if ( $start_date ) : ?>
388 + <span class="wpsubs-cell-title"><?php echo esc_html( wp_date( get_option( 'date_format' ), $start_date ) ); ?></span>
389 + <span class="wpsubs-cell-id"><?php echo esc_html( wp_date( get_option( 'time_format' ), $start_date ) ); ?></span>
390 + <?php else : ?>
391 + <span class="wpsubs-cell--muted">&#8212;</span>
392 + <?php endif; ?>
393 + </td>
394 +
395 + <!-- Renewal date -->
396 + <td class="wpsubs-col--nowrap">
397 + <?php if ( $renewal_date ) : ?>
398 + <span class="wpsubs-cell-title"><?php echo esc_html( wp_date( get_option( 'date_format' ), $renewal_date ) ); ?></span>
399 + <span class="wpsubs-cell-id"><?php echo esc_html( wp_date( get_option( 'time_format' ), $renewal_date ) ); ?></span>
400 + <?php else : ?>
401 + <span class="wpsubs-cell--muted">&#8212;</span>
402 + <?php endif; ?>
403 + </td>
404 +
405 + <!-- Actions -->
406 + <td class="wpsubs-cell--actions">
407 + <div class="wpsubs-row-actions">
408 + <button type="button" class="wpsubs-row-actions__trigger" aria-label="<?php esc_attr_e( 'Row actions', 'subscription' ); ?>">···</button>
409 + <div class="wpsubs-dropdown">
410 + <a href="<?php echo esc_url( $view_url ); ?>" class="wpsubs-dropdown__item"><?php esc_html_e( 'View / Edit', 'subscription' ); ?></a>
411 + <div class="wpsubs-dropdown__divider"></div>
412 + <?php if ( ! $is_trash ) : ?>
413 + <a href="<?php echo esc_url( $trash_url ); ?>" class="wpsubs-dropdown__item wpsubs-dropdown__item--danger" onclick="return confirm('<?php esc_attr_e( 'Move this subscription to trash?', 'subscription' ); ?>')"><?php esc_html_e( 'Trash', 'subscription' ); ?></a>
414 + <?php else : ?>
415 + <a href="<?php echo esc_url( $restore_url ); ?>" class="wpsubs-dropdown__item"><?php esc_html_e( 'Restore', 'subscription' ); ?></a>
416 + <a href="<?php echo esc_url( $delete_url ); ?>" class="wpsubs-dropdown__item wpsubs-dropdown__item--danger" onclick="return confirm('<?php esc_attr_e( 'Delete permanently? This cannot be undone.', 'subscription' ); ?>')"><?php esc_html_e( 'Delete Permanently', 'subscription' ); ?></a>
417 + <?php endif; ?>
418 + </div>
419 + </div>
420 + </td>
421 + </tr>
422 + <?php endforeach; ?>
423 +
424 + <?php else : ?>
425 + <tr>
426 + <td colspan="8">
427 + <div class="wpsubs-empty">
428 + <div class="wpsubs-empty__icon">📋</div>
429 + <div class="wpsubs-empty__title"><?php esc_html_e( 'No subscriptions found', 'subscription' ); ?></div>
430 + <div class="wpsubs-empty__desc"><?php esc_html_e( 'Subscriptions will appear here once customers complete checkout.', 'subscription' ); ?></div>
431 + </div>
432 + </td>
433 + </tr>
434 + <?php endif; ?>
435 +
436 + </tbody>
437 + </table>
438 +
439 + <!-- Pagination inside table card -->
440 + <?php
441 + wpsubs_render_pager(
247 442 array(
248 - 'paged' => $i,
249 - 'per_page' => $per_page,
250 - ),
251 - $base_url
443 + 'current' => (int) $paged,
444 + 'total' => max( 1, (int) $max_num_pages ),
445 + 'info' => true,
446 + 'per_page' => (int) $per_page,
447 + 'item_count' => (int) $total,
448 + 'base_url' => remove_query_arg( 'paged' ),
449 + 'link_mode' => 'url',
450 + // translators: %1$s = first item number, %2$s = last item number, %3$s = total items.
451 + 'info_format' => __( 'Showing %1$s–%2$s of %3$s subscriptions', 'subscription' ),
452 + 'context' => 'subscription-list',
453 + )
252 454 );
253 - $is_current = $i == $paged;
254 455 ?>
255 - <a href="<?php echo esc_url( $url ); ?>" class="button
256 - <?php
257 - if ( $is_current ) {
258 - echo ' button-primary';}
259 - ?>
260 - "
261 - <?php
262 - if ( $is_current ) {
263 - echo 'disabled';}
264 - ?>
265 -><?php echo (int) $i; ?></a>
266 - <?php endfor; ?>
267 - <span class="goto-label">Go to</span>
268 - <form method="get">
269 - <input type="hidden" name="page" value="wp-subscription" />
270 - <input type="number" name="paged" min="1" max="<?php echo (int) $max_num_pages; ?>" value="<?php echo (int) $paged; ?>" />
271 - <input type="hidden" name="per_page" value="<?php echo (int) $per_page; ?>" />
272 - <button type="submit" class="button">OK</button>
273 - </form>
274 - </div>
275 - <?php endif; ?>
456 +
457 + </div><!-- /.wpsubs-table-card -->
458 +
459 + </form>
460 +
276 461 </div>
462 +
463 +<script>
464 +( function () {
465 + var selectAll = document.getElementById( 'cb-select-all' );
466 + var form = document.getElementById( 'subscriptions-form' );
467 +
468 + function rowChecks() {
469 + return form ? Array.from( form.querySelectorAll( '.wpsubs-row-check' ) ) : [];
470 + }
471 +
472 + function syncSelectAll() {
473 + if ( ! selectAll ) return;
474 + var checks = rowChecks();
475 + var checked = checks.filter( function ( cb ) { return cb.checked; } );
476 + checks.forEach( function ( cb ) {
477 + var row = cb.closest( 'tr' );
478 + if ( row ) row.classList.toggle( 'wpsubs-row--selected', cb.checked );
479 + } );
480 + selectAll.indeterminate = checked.length > 0 && checked.length < checks.length;
481 + selectAll.checked = checks.length > 0 && checked.length === checks.length;
482 + }
483 +
484 + if ( selectAll ) {
485 + selectAll.addEventListener( 'change', function () {
486 + rowChecks().forEach( function ( cb ) { cb.checked = selectAll.checked; } );
487 + syncSelectAll();
488 + } );
489 + }
490 +
491 + if ( form ) {
492 + form.addEventListener( 'change', function ( e ) {
493 + if ( e.target.classList.contains( 'wpsubs-row-check' ) ) syncSelectAll();
494 + } );
495 + }
496 +
497 + // ── Advanced-select event handlers ──────────────────────────
498 + var bulkSubmit = document.getElementById( 'wpsubs-bulk-action-submit' );
499 + document.addEventListener( 'wpsubs:select', function ( e ) {
500 + var id = e.target && e.target.id;
501 +
502 + // Bulk action: submit via hidden button
503 + if ( id === 'wpsubs-bulk-action-select' && bulkSubmit ) {
504 + bulkSubmit.click();
505 + return;
506 + }
507 +
508 + // Per-page: auto-submit as a filter action
509 + if ( id === 'wpsubs-per-page-select' && form ) {
510 + var fi = document.createElement( 'input' );
511 + fi.type = 'hidden';
512 + fi.name = 'filter_action';
513 + fi.value = 'filter';
514 + form.appendChild( fi );
515 + form.submit();
516 + }
517 + } );
518 +
519 + // ── Row action dropdowns ─────────────────────────────────────
520 + function closeAllRowMenus() {
521 + document.querySelectorAll( '.wpsubs-row-actions--open' ).forEach( function ( el ) {
522 + el.classList.remove( 'wpsubs-row-actions--open' );
523 + var d = el.querySelector( '.wpsubs-dropdown' );
524 + if ( d ) d.classList.remove( 'wpsubs-dropdown--open' );
525 + } );
526 + }
527 +
528 + document.addEventListener( 'click', function ( e ) {
529 + var trigger = e.target.closest( '.wpsubs-row-actions__trigger' );
530 +
531 + document.querySelectorAll( '.wpsubs-row-actions--open' ).forEach( function ( el ) {
532 + if ( el !== ( trigger && trigger.closest( '.wpsubs-row-actions' ) ) ) {
533 + el.classList.remove( 'wpsubs-row-actions--open' );
534 + var d = el.querySelector( '.wpsubs-dropdown' );
535 + if ( d ) d.classList.remove( 'wpsubs-dropdown--open' );
536 + }
537 + } );
538 +
539 + if ( ! trigger ) return;
540 +
541 + e.stopPropagation();
542 + var wrapper = trigger.closest( '.wpsubs-row-actions' );
543 + var dropdown = wrapper && wrapper.querySelector( '.wpsubs-dropdown' );
544 + if ( ! wrapper || ! dropdown ) return;
545 +
546 + var isOpen = wrapper.classList.contains( 'wpsubs-row-actions--open' );
547 + wrapper.classList.toggle( 'wpsubs-row-actions--open', ! isOpen );
548 + dropdown.classList.toggle( 'wpsubs-dropdown--open', ! isOpen );
549 + } );
550 +
551 + document.addEventListener( 'keydown', function ( e ) {
552 + if ( e.key === 'Escape' ) closeAllRowMenus();
553 + } );
554 +
555 + syncSelectAll();
556 +}() );
557 +</script>